new tools modules & sass opti
All checks were successful
Deploy Sokko G / deploy (push) Successful in 6s
All checks were successful
Deploy Sokko G / deploy (push) Successful in 6s
This commit is contained in:
parent
821e8e0a36
commit
9e275e0167
24 changed files with 3266 additions and 2372 deletions
|
|
@ -4,7 +4,7 @@ import "./styles/main.scss";
|
|||
import { Icon } from "./components/Icon.jsx";
|
||||
import { GameRoute } from "./features/games/GameRoute.jsx";
|
||||
import { GamesPage } from "./features/games/GamesPage.jsx";
|
||||
import { ToolboxModules, TOOLBOX_MODULES } from "./features/toolboxes/modules/index.jsx";
|
||||
import { AddToolControls, ToolboxModules, TOOLBOX_MODULES } from "./features/toolboxes/modules/index.jsx";
|
||||
|
||||
const STORAGE_KEYS = {
|
||||
registry: "sokkog:toolboxes",
|
||||
|
|
@ -14,7 +14,7 @@ const STORAGE_KEYS = {
|
|||
const APP_STORAGE_PREFIX = "sokkog:";
|
||||
const APP_STORAGE_LIMIT_BYTES = 5 * 1024 * 1024;
|
||||
const APP_STORAGE_WARNING_RATIO = 0.85;
|
||||
const ID_PREFIXES = { tbx: "t", mod: "m", item: "i", shot: "s" };
|
||||
const ID_PREFIXES = { tbx: "t", mod: "m", item: "i", shot: "s", link: "l", counter: "c" };
|
||||
const ID_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
||||
|
||||
const DEFAULT_SITE_CONTENT = {
|
||||
|
|
@ -223,6 +223,60 @@ function normalizeChecklistData(data) {
|
|||
};
|
||||
}
|
||||
|
||||
function normalizeUrl(value) {
|
||||
const cleanValue = String(value || "").trim();
|
||||
if (!cleanValue) return "";
|
||||
|
||||
try {
|
||||
const url = new URL(cleanValue.includes("://") ? cleanValue : `https://${cleanValue}`);
|
||||
if (url.protocol !== "http:" && url.protocol !== "https:") return "";
|
||||
return url.href;
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function hostnameFromUrl(value) {
|
||||
try {
|
||||
return new URL(value).hostname.replace(/^www\./, "");
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeLinksData(data) {
|
||||
return {
|
||||
links: (data?.links || [])
|
||||
.map((link) => {
|
||||
const url = normalizeUrl(link?.url);
|
||||
if (!url) return null;
|
||||
return {
|
||||
id: link?.id || uid("link"),
|
||||
title: String(link?.title || "").trim(),
|
||||
url
|
||||
};
|
||||
})
|
||||
.filter(Boolean)
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeCounterValue(value) {
|
||||
const parsed = Number.parseInt(value, 10);
|
||||
return Number.isFinite(parsed) ? parsed : 0;
|
||||
}
|
||||
|
||||
function normalizeCountersData(data) {
|
||||
return {
|
||||
counters: (data?.counters || [])
|
||||
.map((counter) => ({
|
||||
id: counter?.id || uid("counter"),
|
||||
label: String(counter?.label || "").trim(),
|
||||
value: normalizeCounterValue(counter?.value)
|
||||
}))
|
||||
.filter((counter) => counter.label)
|
||||
};
|
||||
}
|
||||
|
||||
function compactChecklistItemForStorage(item) {
|
||||
const normalized = normalizeChecklistItem(item);
|
||||
const compact = { id: normalized.id, label: normalized.label };
|
||||
|
|
@ -246,6 +300,25 @@ function compactModuleDataForStorage(type, value) {
|
|||
.map((shot) => ({ id: shot.id || uid("shot"), dataUrl: shot.dataUrl }));
|
||||
return shots.length ? { shots } : null;
|
||||
}
|
||||
if (type === "links") {
|
||||
const links = normalizeLinksData(value).links.map((link) => {
|
||||
const compact = {
|
||||
id: link.id,
|
||||
url: link.url
|
||||
};
|
||||
if (link.title) compact.title = link.title;
|
||||
return compact;
|
||||
});
|
||||
return links.length ? { links } : null;
|
||||
}
|
||||
if (type === "counters") {
|
||||
const counters = normalizeCountersData(value).counters.map((counter) => ({
|
||||
id: counter.id,
|
||||
label: counter.label,
|
||||
value: counter.value
|
||||
}));
|
||||
return counters.length ? { counters } : null;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
|
|
@ -277,6 +350,15 @@ function remapModuleDataForExport(type, data, nextId) {
|
|||
if (type === "screenshots") {
|
||||
return { shots: compact.shots.map((shot) => ({ ...shot, id: nextId("shot") })) };
|
||||
}
|
||||
|
||||
if (type === "links") {
|
||||
return { links: compact.links.map((link) => ({ ...link, id: nextId("link") })) };
|
||||
}
|
||||
|
||||
if (type === "counters") {
|
||||
return { counters: compact.counters.map((counter) => ({ ...counter, id: nextId("counter") })) };
|
||||
}
|
||||
|
||||
return compact;
|
||||
}
|
||||
|
||||
|
|
@ -386,6 +468,15 @@ function openImageInNewTab(dataUrl) {
|
|||
}
|
||||
}
|
||||
|
||||
async function copyText(value) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(value);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function useHashRoute() {
|
||||
const [route, setRoute] = useState(currentRoute);
|
||||
useEffect(() => {
|
||||
|
|
@ -762,7 +853,7 @@ function Shell({ route, content, games, toolboxes, links, actions, children }) {
|
|||
<span className="badge">{content.sidebar.badge}</span>
|
||||
<p>{content.sidebar.note}</p>
|
||||
<div className="sidebar-storage-actions" aria-label="Actions globales de stockage">
|
||||
<ImportButton className="sidebar-action-button" title="Importer tout" ariaLabel="Importer toutes les toolboxes" onFile={actions.importAllToolboxes} icon="import" />
|
||||
<ImportButton className="sidebar-action-button import-icon-button" title="Importer tout" ariaLabel="Importer toutes les toolboxes" onFile={actions.importAllToolboxes} icon="import" />
|
||||
<button className="sidebar-action-button" onClick={actions.exportAllToolboxes} aria-label="Exporter toutes les toolboxes" title="Exporter tout"><Icon name="export" /></button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -901,9 +992,15 @@ function StorageHelpCard({ help }) {
|
|||
function ToolboxCard({ toolbox, game, actions }) {
|
||||
return (
|
||||
<article className="card toolbox-card">
|
||||
<div className={`card-cover ${game?.image ? "toolbox-card-cover" : ""}`} style={{ "--game-cover": game?.cover || "var(--gradient-nebula)", background: game?.image ? "" : "var(--gradient-nebula)" }}>
|
||||
<a
|
||||
className={`card-cover toolbox-card-cover-link ${game?.image ? "toolbox-card-cover" : ""}`}
|
||||
href={`#/toolbox/${toolbox.id}`}
|
||||
aria-label={`Ouvrir ${toolbox.name}`}
|
||||
title={`Ouvrir ${toolbox.name}`}
|
||||
style={{ "--game-cover": game?.cover || "var(--gradient-nebula)", background: game?.image ? "" : "var(--gradient-nebula)" }}
|
||||
>
|
||||
{game?.image && <img src={game.image} alt={game.title} loading="lazy" />}
|
||||
</div>
|
||||
</a>
|
||||
<div className="card-body">
|
||||
<p className="eyebrow">{game ? game.title : "Toolbox libre"}</p>
|
||||
<h2>{toolbox.name}</h2>
|
||||
|
|
@ -936,6 +1033,11 @@ function ToolboxView({ toolbox, embedded, actions, updateToolbox, updateModuleDa
|
|||
const moduleContext = {
|
||||
getModuleData,
|
||||
normalizeChecklistData,
|
||||
normalizeLinksData,
|
||||
normalizeCountersData,
|
||||
normalizeUrl,
|
||||
hostnameFromUrl,
|
||||
copyText,
|
||||
clampQty,
|
||||
uid,
|
||||
setModuleData: updateModuleData,
|
||||
|
|
@ -971,10 +1073,7 @@ function ToolboxView({ toolbox, embedded, actions, updateToolbox, updateModuleDa
|
|||
)}
|
||||
</div>
|
||||
<div className="actions">
|
||||
<select value="" onChange={(event) => addModule(event.target.value)} aria-label="Ajouter un outil">
|
||||
<option value="">Ajouter un outil</option>
|
||||
{Object.entries(TOOLBOX_MODULES).map(([type, module]) => <option key={type} value={type}>{module.label}</option>)}
|
||||
</select>
|
||||
<AddToolControls onAdd={addModule} />
|
||||
</div>
|
||||
</section>
|
||||
{!embedded && (
|
||||
|
|
@ -1053,7 +1152,7 @@ function ToolboxDrawer({ gameId, toolboxes, links, actions, updateToolbox, updat
|
|||
<div>
|
||||
<p className="eyebrow">Toolbox liée</p>
|
||||
<div className="drawer-toolbox-actions" aria-label="Actions toolbox liée">
|
||||
<ImportButton className="drawer-action-button" ariaLabel="Importer une toolbox" title="Importer" icon="import" onFile={(file) => actions.importToolbox(file, gameId)} />
|
||||
<ImportButton className="drawer-action-button import-icon-button" ariaLabel="Importer une toolbox" title="Importer" icon="import" onFile={(file) => actions.importToolbox(file, gameId)} />
|
||||
<button className="drawer-action-button" onClick={() => actions.setLinkModalGameId(gameId)} aria-label="Lier une toolbox" title="Lier"><Icon name="link" /></button>
|
||||
<button className="drawer-action-button" onClick={() => actions.exportToolbox(selectedId)} disabled={!selectedId} aria-label="Exporter la toolbox liée" title="Exporter"><Icon name="export" /></button>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue