new tools modules & sass opti
All checks were successful
Deploy Sokko G / deploy (push) Successful in 6s

This commit is contained in:
Shinuwa 2026-07-21 13:01:14 +02:00
parent 821e8e0a36
commit 9e275e0167
24 changed files with 3266 additions and 2372 deletions

View file

@ -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>