This commit is contained in:
parent
8b9171d4f9
commit
ef5f977f8a
15 changed files with 3571 additions and 272 deletions
|
|
@ -52,179 +52,6 @@ const TOOLBOX_ICON_FILES = [
|
|||
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", about: "C'est quoi Sokko G ?", mobileGames: "Infos" },
|
||||
sidebar: { badge: "", note: "" },
|
||||
topbar: { dashboard: "", toolbox: "", games: "" },
|
||||
gamesPage: {
|
||||
eyebrow: "",
|
||||
title: "Jeux",
|
||||
description: "",
|
||||
emptyTitle: "Aucun jeu disponible",
|
||||
emptyText: ""
|
||||
},
|
||||
home: {
|
||||
hero: {
|
||||
eyebrow: "",
|
||||
title: "Sokko G",
|
||||
description: "",
|
||||
primaryAction: "Toolboxes",
|
||||
secondaryAction: "Jeux"
|
||||
},
|
||||
stats: {
|
||||
toolboxSingular: "toolbox",
|
||||
toolboxPlural: "toolboxes",
|
||||
toolSingular: "outil",
|
||||
toolPlural: "outils"
|
||||
},
|
||||
origin: {
|
||||
eyebrow: "",
|
||||
title: "",
|
||||
visualAlt: "",
|
||||
dialogueAriaLabel: "",
|
||||
caption: "",
|
||||
lines: []
|
||||
},
|
||||
aboutLink: "Comprendre Sokko G",
|
||||
legal: {
|
||||
copyright: "",
|
||||
disclaimer: ""
|
||||
}
|
||||
},
|
||||
about: {
|
||||
eyebrow: "À propos",
|
||||
title: "C'est quoi Sokko G ?",
|
||||
description: "Sokko G est une webapp locale pensée pour garder les bons outils et les bonnes informations à portée de main pendant une session gaming.",
|
||||
sections: [],
|
||||
toolsTitle: "Outils disponibles",
|
||||
contribute: {
|
||||
eyebrow: "Contribuer",
|
||||
title: "Proposer une liste de jeu",
|
||||
description: "Les listes servent à transformer des données de jeu en checklists prêtes à l’emploi : équipements à fabriquer, créatures à capturer, monstres à battre, objets à suivre, etc. Une bonne liste est structurée par catégories, avec des noms clairs, une courte description utile et une quantité uniquement quand elle a du sens.\n\nExemple :",
|
||||
example: "{\n \"titre\": \"Équipements\",\n \"equipements\": [\n {\n \"nom\": \"Set d'armure sombre\",\n \"description\": \"Obtenir en quête\",\n \"quantite\": 1\n }\n ]\n}",
|
||||
promptTitle: "Prompt utile",
|
||||
promptIntro: "Ajoutez cette précision à votre demande pour obtenir un format directement exploitable :",
|
||||
promptText: "Génère uniquement un JSON valide.\nLe JSON doit contenir une propriété racine \"titre\", puis une ou plusieurs catégories.\nChaque catégorie doit être un tableau d’objets.\nChaque objet doit contenir \"nom\" et \"description\".\nLa propriété \"quantite\" est facultative ; si elle est absente, elle sera interprétée comme 1.\nRegroupe les éléments de manière logique.\nNe renvoie aucun texte, aucune explication et aucun Markdown."
|
||||
},
|
||||
tools: [
|
||||
{ icon: "notepad", name: "Bloc notes", description: "Notes rapides pendant une session." },
|
||||
{ icon: "checklist", name: "Checklist", description: "Objectifs et quantités à suivre." },
|
||||
{ icon: "picture", name: "Images", description: "Images ajoutées par fichier ou collage." },
|
||||
{ icon: "map", name: "Annotation d'images", description: "Marqueurs sur une carte ou un plan." },
|
||||
{ icon: "link", name: "Liens", description: "Raccourcis vers guides, builds ou cartes." },
|
||||
{ icon: "abacus", name: "Compteurs", description: "Scores, ressources, essais ou résultats." },
|
||||
{ icon: "calculator", name: "Calculateur", description: "Calculs enregistrés et chaînes de craft." }
|
||||
],
|
||||
limitsTitle: "À retenir",
|
||||
limits: []
|
||||
},
|
||||
toolboxes: {
|
||||
eyebrow: "",
|
||||
title: "Toolboxes",
|
||||
newButton: "Nouvelle toolbox",
|
||||
importAll: "Importer",
|
||||
exportAll: "Exporter",
|
||||
importOne: "Importer",
|
||||
summary: "",
|
||||
storageHelp: {
|
||||
title: "",
|
||||
text: "",
|
||||
items: []
|
||||
},
|
||||
modules: {
|
||||
notepad: { placeholder: "Notes rapides..." },
|
||||
checklist: {
|
||||
itemPlaceholder: "Nouvel item",
|
||||
sectionPlaceholder: "Catégorie",
|
||||
quantityLabel: "Quantité cible",
|
||||
addButton: "Ajouter",
|
||||
importPlaceholder: "Importer plusieurs items...\n# Potions\nPotion:10\nMéga potion:5",
|
||||
importOpenButton: "Importer du texte",
|
||||
importModalTitle: "Importer une checklist",
|
||||
importButton: "Importer le texte",
|
||||
hideCompletedSectionsTitle: "Masquer les catégories terminées",
|
||||
showCompletedSectionTitle: "Afficher cette catégorie terminée",
|
||||
hideCompletedSectionTitle: "Masquer cette catégorie terminée",
|
||||
decrementLabel: "Retirer une quantité",
|
||||
incrementLabel: "Ajouter une quantité",
|
||||
currentQuantityLabel: "Quantité actuelle",
|
||||
deleteTitle: "Supprimer"
|
||||
},
|
||||
screenshots: {
|
||||
addImages: "Ajouter des images",
|
||||
pastePlaceholder: "Coller une image ici",
|
||||
pasteAriaLabel: "Coller une image depuis le presse-papiers",
|
||||
imageAlt: "Image",
|
||||
previewAriaLabel: "Agrandir l'image",
|
||||
annotateAriaLabel: "Annoter l'image",
|
||||
annotateTitle: "Annoter",
|
||||
deleteAriaLabel: "Supprimer l'image",
|
||||
deleteTitle: "Supprimer"
|
||||
},
|
||||
links: {
|
||||
titlePlaceholder: "Nom du lien",
|
||||
urlPlaceholder: "https://...",
|
||||
addButton: "Ajouter",
|
||||
importPlaceholder: "Importer plusieurs liens...\nBuild:https://example.com/build?class=rogue\nMap:https://example.com/map",
|
||||
importOpenButton: "Importer du texte",
|
||||
importModalTitle: "Importer des liens",
|
||||
importButton: "Importer le texte",
|
||||
copyTitle: "Copier",
|
||||
copiedTitle: "Copié",
|
||||
deleteTitle: "Supprimer"
|
||||
},
|
||||
counters: {
|
||||
labelPlaceholder: "Nom du compteur",
|
||||
addButton: "Ajouter",
|
||||
decrementLabel: "Décrémenter",
|
||||
incrementLabel: "Incrémenter",
|
||||
resetTitle: "Réinitialiser",
|
||||
deleteTitle: "Supprimer"
|
||||
},
|
||||
calculator: {
|
||||
expressionLabel: "Calcul",
|
||||
expressionPlaceholder: "10*10",
|
||||
resultLabel: "Résultat",
|
||||
labelLabel: "Libellé",
|
||||
labelPlaceholder: "Lingots de fer",
|
||||
saveButton: "Enregistrer",
|
||||
rootButton: "Revenir à la racine",
|
||||
resetTitle: "Réinitialiser",
|
||||
copyTitle: "Copier pour checklist",
|
||||
copiedTitle: "Copié",
|
||||
scrollableTitle: "Liste scrollable",
|
||||
emptyResults: "Aucun résultat enregistré.",
|
||||
readonlyValueTitle: "Quantité non modifiable",
|
||||
useEntryTitle: "Utiliser comme base",
|
||||
renameTitle: "Renommer",
|
||||
deleteTitle: "Supprimer"
|
||||
},
|
||||
imageAnnotation: {
|
||||
addImage: "Ajouter une image",
|
||||
replaceImage: "Remplacer l'image",
|
||||
pastePlaceholder: "Coller une image ici",
|
||||
pasteAriaLabel: "Coller une image à annoter depuis le presse-papiers",
|
||||
stageAriaLabel: "Ajouter un marqueur sur l'image",
|
||||
addMarkerAriaLabel: "Ajouter un marqueur sur l'image",
|
||||
imageAlt: "Image annotée",
|
||||
previewTitle: "Agrandir",
|
||||
previewAriaLabel: "Agrandir l'image annotée",
|
||||
markerPrefix: "Marqueur",
|
||||
markersSingular: "marqueur",
|
||||
markersPlural: "marqueurs",
|
||||
markerPlaceholder: "Libellé du marqueur",
|
||||
emptyImage: "Ajoutez une image pour commencer l'annotation.",
|
||||
emptyMarkers: "Agrandissez l'image pour ajouter un marqueur.",
|
||||
deleteImageTitle: "Supprimer l'image",
|
||||
deleteMarkerTitle: "Supprimer"
|
||||
}
|
||||
},
|
||||
emptyTitle: "Aucune toolbox",
|
||||
emptyText: ""
|
||||
}
|
||||
};
|
||||
|
||||
function randomToken(length = 6) {
|
||||
const bytes = new Uint8Array(length);
|
||||
if (globalThis.crypto?.getRandomValues) {
|
||||
|
|
@ -345,13 +172,14 @@ function normalizeChecklistSection(section, fallbackTitle = "") {
|
|||
|
||||
function normalizeChecklistData(data) {
|
||||
const hideCompletedSections = Boolean(data?.hideCompletedSections);
|
||||
const hideCompletedSectionsFully = Boolean(data?.hideCompletedSectionsFully);
|
||||
const sections = Array.isArray(data?.sections)
|
||||
? data.sections.map((section) => normalizeChecklistSection(section)).filter((section) => section.items.length || section.title)
|
||||
: [];
|
||||
const legacyItems = (data?.items || []).map(normalizeChecklistItem).filter((item) => item.label);
|
||||
if (!sections.length && legacyItems.length) return { hideCompletedSections, sections: [{ id: uid("section"), title: "", items: legacyItems }], items: legacyItems };
|
||||
if (!sections.length && legacyItems.length) return { hideCompletedSections, hideCompletedSectionsFully, sections: [{ id: uid("section"), title: "", items: legacyItems }], items: legacyItems };
|
||||
const items = sections.flatMap((section) => section.items);
|
||||
return { hideCompletedSections, sections, items };
|
||||
return { hideCompletedSections, hideCompletedSectionsFully, sections, items };
|
||||
}
|
||||
|
||||
function normalizeUrl(value) {
|
||||
|
|
@ -475,7 +303,10 @@ function compactModuleDataForStorage(type, value) {
|
|||
const normalized = normalizeChecklistData(value);
|
||||
const sections = normalized.sections.map(compactChecklistSectionForStorage).filter((section) => section.items.length || section.title);
|
||||
if (!sections.length) return null;
|
||||
const settings = normalized.hideCompletedSections ? { hideCompletedSections: true } : {};
|
||||
const settings = {
|
||||
...(normalized.hideCompletedSections ? { hideCompletedSections: true } : {}),
|
||||
...(normalized.hideCompletedSectionsFully ? { hideCompletedSectionsFully: true } : {})
|
||||
};
|
||||
if (sections.length === 1 && !sections[0].title && typeof sections[0].hideWhenComplete !== "boolean") return { ...settings, items: sections[0].items };
|
||||
return { ...settings, sections };
|
||||
}
|
||||
|
|
@ -649,16 +480,6 @@ function formatDate(value) {
|
|||
return new Intl.DateTimeFormat("fr-FR", { dateStyle: "medium", timeStyle: "short" }).format(new Date(value));
|
||||
}
|
||||
|
||||
function mergeContent(defaults, overrides) {
|
||||
if (!overrides || typeof overrides !== "object" || Array.isArray(overrides)) return defaults;
|
||||
return Object.fromEntries(Object.entries(defaults).map(([key, value]) => {
|
||||
const override = overrides[key];
|
||||
if (Array.isArray(value)) return [key, Array.isArray(override) ? override : value];
|
||||
if (value && typeof value === "object") return [key, mergeContent(value, override)];
|
||||
return [key, override ?? value];
|
||||
}));
|
||||
}
|
||||
|
||||
function currentRoute() {
|
||||
const hashRoute = location.hash.replace(/^#/, "");
|
||||
if (hashRoute) return hashRoute.replace(/#.+$/, "");
|
||||
|
|
@ -845,7 +666,8 @@ function useIndexedToolboxes(onError) {
|
|||
|
||||
function App() {
|
||||
const route = useHashRoute();
|
||||
const [siteContent, setSiteContent] = useState(DEFAULT_SITE_CONTENT);
|
||||
const [siteContent, setSiteContent] = useState(null);
|
||||
const [siteContentError, setSiteContentError] = useState("");
|
||||
const [games, setGames] = useState([]);
|
||||
const [gamesError, setGamesError] = useState("");
|
||||
const [mhwilds, setMhwilds] = useState(INITIAL_MHWILDS_STATE);
|
||||
|
|
@ -885,13 +707,19 @@ function App() {
|
|||
|
||||
useEffect(() => {
|
||||
Promise.all([
|
||||
fetch("/data/site.json").then((response) => response.ok ? response.json() : DEFAULT_SITE_CONTENT).catch(() => DEFAULT_SITE_CONTENT),
|
||||
fetch("/data/site.json").then((response) => {
|
||||
if (!response.ok) throw new Error("Impossible de charger le contenu du site.");
|
||||
return response.json();
|
||||
}).catch((error) => {
|
||||
setSiteContentError(error.message);
|
||||
return null;
|
||||
}),
|
||||
fetch("/data/games.json").then((response) => response.ok ? response.json() : { games: [] }).catch((error) => {
|
||||
setGamesError(error.message);
|
||||
return { games: [] };
|
||||
})
|
||||
]).then(([content, gamesPayload]) => {
|
||||
setSiteContent(mergeContent(DEFAULT_SITE_CONTENT, content));
|
||||
if (content) setSiteContent(content);
|
||||
setGames(Array.isArray(gamesPayload.games) ? gamesPayload.games : []);
|
||||
});
|
||||
}, []);
|
||||
|
|
@ -1129,6 +957,17 @@ function App() {
|
|||
}
|
||||
};
|
||||
|
||||
if (!siteContent) {
|
||||
return (
|
||||
<main className="startup-error">
|
||||
<section className="empty">
|
||||
<h1>Contenu indisponible</h1>
|
||||
<p>{siteContentError || "Chargement du contenu..."}</p>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Shell route={route} content={siteContent} games={games} toolboxes={store.toolboxes} links={store.links} actions={actions}>
|
||||
<RouteContent
|
||||
|
|
@ -1300,7 +1139,7 @@ function HomePage({ siteContent, toolboxes }) {
|
|||
}
|
||||
|
||||
function AboutPage({ siteContent }) {
|
||||
const content = siteContent.about || DEFAULT_SITE_CONTENT.about;
|
||||
const content = siteContent.about;
|
||||
const limits = Array.isArray(content.limits) ? content.limits : [];
|
||||
const storageSectionIndex = content.sections.findIndex((section) => section.title.toLowerCase().includes("stockage"));
|
||||
const normalizeReminder = (item, index) => typeof item === "string" ? { title: `Point ${index + 1}`, text: item, icon: "toolbox" } : item;
|
||||
|
|
@ -1628,7 +1467,7 @@ function ToolboxGameIcon({ game }) {
|
|||
function ToolboxView({ siteContent, toolbox, toolboxGame, embedded, actions, storageUsage, getModuleData, updateToolbox, updateModuleData, addScreenshotFiles }) {
|
||||
const moduleColumns = Number(toolbox.moduleColumns) === 1 ? 1 : 2;
|
||||
const toolboxGameCover = getGameCardCover(toolboxGame);
|
||||
const moduleText = siteContent?.toolboxes?.modules || DEFAULT_SITE_CONTENT.toolboxes.modules;
|
||||
const moduleText = siteContent.toolboxes.modules;
|
||||
const moduleContext = {
|
||||
getModuleData,
|
||||
moduleText,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue