improve style & add icon toolboxes
All checks were successful
Deploy Sokko G / deploy (push) Successful in 5s
All checks were successful
Deploy Sokko G / deploy (push) Successful in 5s
This commit is contained in:
parent
9e275e0167
commit
0493db233a
31 changed files with 317 additions and 57 deletions
|
|
@ -16,12 +16,43 @@ 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", link: "l", counter: "c" };
|
||||
const ID_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
||||
const TOOLBOX_ICON_BASE = "/static/img/toolbox-icons/";
|
||||
const TOOLBOX_ICON_FILES = [
|
||||
"toolbox.png",
|
||||
"ball.png",
|
||||
"capture.png",
|
||||
"card.png",
|
||||
"city.png",
|
||||
"compass.png",
|
||||
"crash.png",
|
||||
"detective.png",
|
||||
"fire.png",
|
||||
"horror.png",
|
||||
"jump.png",
|
||||
"mining.png",
|
||||
"parachute.png",
|
||||
"puzzle.png",
|
||||
"shield.png",
|
||||
"sword.png",
|
||||
"target.png",
|
||||
"tower.png",
|
||||
"wheel.png"
|
||||
];
|
||||
const TOOLBOX_ICONS = TOOLBOX_ICON_FILES.map((file) => `${TOOLBOX_ICON_BASE}${file}`);
|
||||
const DEFAULT_TOOLBOX_ICON = `${TOOLBOX_ICON_BASE}toolbox.png`;
|
||||
|
||||
const DEFAULT_SITE_CONTENT = {
|
||||
brand: { name: "Sokko G", homeAriaLabel: "Accueil Sokko G" },
|
||||
navigation: { home: "Accueil", toolboxes: "Toolboxes", games: "Jeux", mobileGames: "Infos" },
|
||||
sidebar: { badge: "Local only", note: "Données stockées dans ce navigateur." },
|
||||
topbar: { dashboard: "Dashboard", toolbox: "Toolbox active", games: "Informations jeu" },
|
||||
gamesPage: {
|
||||
eyebrow: "Pages informatives",
|
||||
title: "Jeux disponibles",
|
||||
description: "Choisissez un jeu pour consulter ses données maintenues et associer une toolbox locale.",
|
||||
emptyTitle: "Aucun jeu disponible",
|
||||
emptyText: "Ajoutez des entrées dans /data/games.json."
|
||||
},
|
||||
home: {
|
||||
hero: {
|
||||
eyebrow: "Session gaming efficace",
|
||||
|
|
@ -52,6 +83,7 @@ const DEFAULT_SITE_CONTENT = {
|
|||
importAll: "Importer tout",
|
||||
exportAll: "Exporter tout",
|
||||
importOne: "Importer une toolbox",
|
||||
summary: "Créez des espaces modulaires pour regrouper vos outils de session : notes, suivis, références, captures et futurs modules restent accessibles rapidement, en local, par jeu ou par usage.",
|
||||
storageHelp: {
|
||||
title: "Sauvegardes locales",
|
||||
text: "Vos toolboxes restent sur cet appareil, dans ce navigateur. Rien n’est envoyé sur un serveur.",
|
||||
|
|
@ -128,6 +160,12 @@ function getDefaultModuleTitle(type) {
|
|||
return TOOLBOX_MODULES[type]?.label || "Outil";
|
||||
}
|
||||
|
||||
function normalizeToolboxIcon(icon) {
|
||||
const value = String(icon || "").trim();
|
||||
const file = value.replace(TOOLBOX_ICON_BASE, "").split("/").pop();
|
||||
return TOOLBOX_ICON_FILES.includes(file) ? `${TOOLBOX_ICON_BASE}${file}` : DEFAULT_TOOLBOX_ICON;
|
||||
}
|
||||
|
||||
function normalizeToolboxModule(module) {
|
||||
if (!module || typeof module !== "object" || !module.type) return null;
|
||||
const title = String(module.title || "").trim();
|
||||
|
|
@ -141,6 +179,7 @@ function normalizeToolbox(toolbox) {
|
|||
return {
|
||||
id: toolbox.id || uid("tbx"),
|
||||
name: String(toolbox.name || "Nouvelle toolbox").trim() || "Nouvelle toolbox",
|
||||
icon: normalizeToolboxIcon(toolbox.icon),
|
||||
moduleColumns: Number(toolbox.moduleColumns) === 1 ? 1 : 2,
|
||||
modules: (Array.isArray(toolbox.modules) ? toolbox.modules : []).map(normalizeToolboxModule).filter(Boolean),
|
||||
updatedAt: toolbox.updatedAt || new Date().toISOString()
|
||||
|
|
@ -157,6 +196,7 @@ function compactToolboxForStorage(toolbox) {
|
|||
updatedAt: normalized.updatedAt
|
||||
};
|
||||
if (normalized.moduleColumns === 1) compact.moduleColumns = 1;
|
||||
if (normalized.icon !== DEFAULT_TOOLBOX_ICON) compact.icon = normalized.icon;
|
||||
return compact;
|
||||
}
|
||||
|
||||
|
|
@ -935,9 +975,15 @@ function DialogueLine({ line }) {
|
|||
|
||||
function RichText({ text }) {
|
||||
const parts = String(text).split(/(<i>.*?<\/i>)/g).filter(Boolean);
|
||||
return parts.map((part, index) => {
|
||||
return parts.flatMap((part, index) => {
|
||||
const italic = part.match(/^<i>(.*?)<\/i>$/);
|
||||
return italic ? <i key={index}>{italic[1]}</i> : <React.Fragment key={index}>{part}</React.Fragment>;
|
||||
const value = italic ? italic[1] : part;
|
||||
const lines = value.split("\n");
|
||||
return lines.flatMap((line, lineIndex) => {
|
||||
const key = `${index}-${lineIndex}`;
|
||||
const node = italic ? <i key={key}>{line}</i> : <React.Fragment key={key}>{line}</React.Fragment>;
|
||||
return lineIndex === lines.length - 1 ? [node] : [node, <br key={`${key}-br`} />];
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -950,15 +996,18 @@ function ToolboxesPage({ siteContent, toolboxes, getToolboxGame, actions }) {
|
|||
<p className="eyebrow">{content.eyebrow}</p>
|
||||
<h1>{content.title}</h1>
|
||||
</div>
|
||||
<div className="actions">
|
||||
<button className="primary" onClick={() => actions.setCreateModal({})}>{content.newButton}</button>
|
||||
<ImportButton className="import-button" label={content.importOne} onFile={actions.importToolbox} />
|
||||
</div>
|
||||
</section>
|
||||
<section className="toolbar toolbar-stack">
|
||||
<div>
|
||||
<ImportButton className="import-button" label={content.importAll} onFile={actions.importAllToolboxes} />
|
||||
<button onClick={actions.exportAllToolboxes}>{content.exportAll}</button>
|
||||
<p className="toolbox-summary">{content.summary}</p>
|
||||
<div className="toolbox-actions-row">
|
||||
<div>
|
||||
<button className="primary" onClick={() => actions.setCreateModal({})}>{content.newButton}</button>
|
||||
<ImportButton className="import-button" label={content.importOne} onFile={actions.importToolbox} />
|
||||
</div>
|
||||
<div>
|
||||
<ImportButton className="import-button" label={content.importAll} onFile={actions.importAllToolboxes} />
|
||||
<button onClick={actions.exportAllToolboxes}>{content.exportAll}</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section className="cards">
|
||||
|
|
@ -990,16 +1039,17 @@ function StorageHelpCard({ help }) {
|
|||
}
|
||||
|
||||
function ToolboxCard({ toolbox, game, actions }) {
|
||||
const coverImage = game?.image || toolbox.icon || DEFAULT_TOOLBOX_ICON;
|
||||
return (
|
||||
<article className="card toolbox-card">
|
||||
<a
|
||||
className={`card-cover toolbox-card-cover-link ${game?.image ? "toolbox-card-cover" : ""}`}
|
||||
className={`card-cover toolbox-card-cover-link ${game?.image ? "toolbox-card-cover" : "toolbox-icon-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" />}
|
||||
<img src={coverImage} alt={game ? game.title : `Icône de ${toolbox.name}`} loading="lazy" />
|
||||
</a>
|
||||
<div className="card-body">
|
||||
<p className="eyebrow">{game ? game.title : "Toolbox libre"}</p>
|
||||
|
|
@ -1022,13 +1072,83 @@ function ToolboxCard({ toolbox, game, actions }) {
|
|||
}
|
||||
|
||||
function ToolboxPage(props) {
|
||||
const { toolboxId, toolboxes } = props;
|
||||
const { toolboxId, toolboxes, getToolboxGame } = props;
|
||||
const toolbox = toolboxes.find((item) => item.id === toolboxId);
|
||||
if (!toolbox) return <div className="empty"><h1>Toolbox introuvable</h1><a className="button" href="#/toolboxes">Retour</a></div>;
|
||||
return <ToolboxView {...props} toolbox={toolbox} embedded={false} />;
|
||||
return <ToolboxView {...props} toolbox={toolbox} toolboxGame={getToolboxGame(toolbox)} embedded={false} />;
|
||||
}
|
||||
|
||||
function ToolboxView({ toolbox, embedded, actions, updateToolbox, updateModuleData, addScreenshotFiles }) {
|
||||
function ToolboxIconPicker({ toolbox, onChange }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const pickerRef = useRef(null);
|
||||
const icon = normalizeToolboxIcon(toolbox.icon);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return undefined;
|
||||
|
||||
function handlePointerDown(event) {
|
||||
if (!pickerRef.current?.contains(event.target)) setOpen(false);
|
||||
}
|
||||
|
||||
function handleKeyDown(event) {
|
||||
if (event.key === "Escape") setOpen(false);
|
||||
}
|
||||
|
||||
document.addEventListener("pointerdown", handlePointerDown);
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
return () => {
|
||||
document.removeEventListener("pointerdown", handlePointerDown);
|
||||
document.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, [open]);
|
||||
|
||||
function selectIcon(nextIcon) {
|
||||
onChange(nextIcon);
|
||||
setOpen(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="toolbox-icon-picker" ref={pickerRef}>
|
||||
<button
|
||||
className="toolbox-icon-picker-button"
|
||||
type="button"
|
||||
onClick={() => setOpen((value) => !value)}
|
||||
aria-label="Changer l'icône de la toolbox"
|
||||
aria-expanded={open}
|
||||
title="Changer l'icône"
|
||||
>
|
||||
<img src={icon} alt="" aria-hidden="true" />
|
||||
</button>
|
||||
{open && (
|
||||
<div className="toolbox-icon-picker-menu" aria-label="Icônes de toolbox">
|
||||
{TOOLBOX_ICONS.map((path) => (
|
||||
<button
|
||||
key={path}
|
||||
className={path === icon ? "active" : ""}
|
||||
type="button"
|
||||
onClick={() => selectIcon(path)}
|
||||
aria-label={`Utiliser l'icône ${path.split("/").pop().replace(".png", "")}`}
|
||||
aria-pressed={path === icon}
|
||||
title={path.split("/").pop().replace(".png", "")}
|
||||
>
|
||||
<img src={path} alt="" aria-hidden="true" loading="lazy" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ToolboxGameIcon({ game }) {
|
||||
return (
|
||||
<div className="toolbox-game-icon" title={game.title} aria-label={`Jeu associé : ${game.title}`}>
|
||||
<img src={game.image} alt="" aria-hidden="true" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ToolboxView({ toolbox, toolboxGame, embedded, actions, updateToolbox, updateModuleData, addScreenshotFiles }) {
|
||||
const moduleColumns = Number(toolbox.moduleColumns) === 1 ? 1 : 2;
|
||||
const moduleContext = {
|
||||
getModuleData,
|
||||
|
|
@ -1068,9 +1188,13 @@ function ToolboxView({ toolbox, embedded, actions, updateToolbox, updateModuleDa
|
|||
<section className="toolbox-head">
|
||||
<div>
|
||||
<p className="eyebrow">Toolbox</p>
|
||||
{embedded ? <h1>{toolbox.name}</h1> : (
|
||||
<EditableTitle className="toolbox-title" value={toolbox.name} fallback="Nouvelle toolbox" onSave={(name) => name !== toolbox.name && updateToolbox({ ...toolbox, name })} />
|
||||
)}
|
||||
<div className="toolbox-title-row">
|
||||
{!embedded && !toolboxGame && <ToolboxIconPicker toolbox={toolbox} onChange={(icon) => updateToolbox({ ...toolbox, icon })} />}
|
||||
{!embedded && toolboxGame?.image && <ToolboxGameIcon game={toolboxGame} />}
|
||||
{embedded ? <h1>{toolbox.name}</h1> : (
|
||||
<EditableTitle className="toolbox-title" value={toolbox.name} fallback="Nouvelle toolbox" onSave={(name) => name !== toolbox.name && updateToolbox({ ...toolbox, name })} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="actions">
|
||||
<AddToolControls onAdd={addModule} />
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue