add list page and multiples optis
All checks were successful
Deploy Sokko G / deploy (push) Successful in 7s
All checks were successful
Deploy Sokko G / deploy (push) Successful in 7s
This commit is contained in:
parent
d9d22683dc
commit
959fa42454
30 changed files with 1015 additions and 45 deletions
|
|
@ -98,6 +98,15 @@ const DEFAULT_SITE_CONTENT = {
|
|||
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." },
|
||||
|
|
@ -652,11 +661,12 @@ function mergeContent(defaults, overrides) {
|
|||
|
||||
function currentRoute() {
|
||||
const hashRoute = location.hash.replace(/^#/, "");
|
||||
if (hashRoute) return hashRoute;
|
||||
if (hashRoute) return hashRoute.replace(/#.+$/, "");
|
||||
const path = location.pathname.replace(/\/+$/, "") || "/";
|
||||
if (path === "/mhwilds") return "/games/mhwilds";
|
||||
if (path === "/mhwilds/monsters") return "/games/mhwilds/monsters";
|
||||
if (path === "/mhwilds/endemic") return "/games/mhwilds/endemic";
|
||||
if (path === "/mhwilds/lists") return "/games/mhwilds/lists";
|
||||
return path;
|
||||
}
|
||||
|
||||
|
|
@ -850,9 +860,29 @@ function App() {
|
|||
const [linkModalGameId, setLinkModalGameId] = useState("");
|
||||
const [drawerGameId, setDrawerGameId] = useState("");
|
||||
const [screenshot, setScreenshot] = useState(null);
|
||||
const [notification, setNotification] = useState(null);
|
||||
const [storageError, setStorageError] = useState("");
|
||||
const store = useIndexedToolboxes((message) => setStorageError(message));
|
||||
|
||||
function notify(message) {
|
||||
setNotification({ id: Date.now(), message });
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
function handleNotification(event) {
|
||||
if (event.detail?.message) notify(event.detail.message);
|
||||
}
|
||||
|
||||
window.addEventListener("sokkog:notify", handleNotification);
|
||||
return () => window.removeEventListener("sokkog:notify", handleNotification);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!notification) return undefined;
|
||||
const timeoutId = window.setTimeout(() => setNotification(null), 1800);
|
||||
return () => window.clearTimeout(timeoutId);
|
||||
}, [notification]);
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([
|
||||
fetch("/data/site.json").then((response) => response.ok ? response.json() : DEFAULT_SITE_CONTENT).catch(() => DEFAULT_SITE_CONTENT),
|
||||
|
|
@ -945,6 +975,41 @@ function App() {
|
|||
return store.updateModuleData(toolboxId, moduleId, data);
|
||||
}
|
||||
|
||||
function createChecklistFromList(gameId, list) {
|
||||
const toolboxId = store.links[gameId] || "";
|
||||
const toolbox = store.toolboxes.find((item) => item.id === toolboxId);
|
||||
if (!toolbox || !list) return false;
|
||||
|
||||
const moduleId = uid("mod");
|
||||
const sections = (list.categories || []).map((category) => ({
|
||||
id: uid("section"),
|
||||
title: category.title || "",
|
||||
items: (category.items || []).map((item) => ({
|
||||
id: uid("item"),
|
||||
label: item.name,
|
||||
qtyTarget: Math.max(1, Number.parseInt(item.quantity, 10) || 1),
|
||||
qtyCurrent: 0
|
||||
})).filter((item) => item.label)
|
||||
})).filter((section) => section.items.length);
|
||||
|
||||
if (!sections.length) return false;
|
||||
|
||||
setConfirmModal({
|
||||
title: "Créer une checklist",
|
||||
message: `Créer la checklist "${list.title || "Checklist"}" dans la toolbox "${toolbox.name}" ?`,
|
||||
confirmLabel: "Créer",
|
||||
onResolve: (confirmed) => {
|
||||
if (!confirmed) return;
|
||||
store.setToolboxes(store.toolboxes.map((item) => item.id === toolboxId
|
||||
? { ...item, modules: [...item.modules, { id: moduleId, type: "checklist", title: list.title || "Checklist" }], updatedAt: new Date().toISOString() }
|
||||
: item));
|
||||
store.updateModuleData(toolboxId, moduleId, { sections }, "checklist");
|
||||
notify("Checklist créée dans la toolbox associée.");
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
async function importToolbox(file, gameId = "") {
|
||||
const payload = JSON.parse(await file.text());
|
||||
if (!payload.toolbox?.modules) throw new Error("Format d'outils invalide");
|
||||
|
|
@ -1011,7 +1076,9 @@ function App() {
|
|||
setLinkModalGameId,
|
||||
setDrawerGameId,
|
||||
setScreenshot,
|
||||
createChecklistFromList,
|
||||
addScreenshotFiles,
|
||||
notify,
|
||||
removeModuleData: store.removeModuleData,
|
||||
updateModuleData: store.updateModuleData,
|
||||
updateToolboxOrder: (orderedIds) => {
|
||||
|
|
@ -1020,7 +1087,9 @@ function App() {
|
|||
},
|
||||
importToolbox: async (file, gameId = "") => {
|
||||
try {
|
||||
return await importToolbox(file, gameId);
|
||||
const toolbox = await importToolbox(file, gameId);
|
||||
if (toolbox) notify("Toolbox importée.");
|
||||
return toolbox;
|
||||
} catch (error) {
|
||||
setConfirmModal({
|
||||
title: "Import impossible",
|
||||
|
|
@ -1035,6 +1104,7 @@ function App() {
|
|||
importAllToolboxes: async (file) => {
|
||||
try {
|
||||
await importAllToolboxes(file);
|
||||
notify("Import global terminé.");
|
||||
return true;
|
||||
} catch (error) {
|
||||
setConfirmModal({
|
||||
|
|
@ -1051,8 +1121,12 @@ function App() {
|
|||
const toolbox = normalizeToolbox(store.toolboxes.find((item) => item.id === id));
|
||||
if (!toolbox) return;
|
||||
downloadJson(createToolboxExportPayload(toolbox, store.moduleData), `${toolbox.name.toLowerCase().replace(/[^a-z0-9]+/gi, "-")}.sokkog.json`);
|
||||
notify("Export de la toolbox lancé.");
|
||||
},
|
||||
exportAllToolboxes: () => downloadJson(createGlobalExportPayload(store.toolboxes, store.links, store.moduleData), `sokkog-global-${new Date().toISOString().slice(0, 10)}.json`)
|
||||
exportAllToolboxes: () => {
|
||||
downloadJson(createGlobalExportPayload(store.toolboxes, store.links, store.moduleData), `sokkog-global-${new Date().toISOString().slice(0, 10)}.json`);
|
||||
notify("Export global lancé.");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -1115,6 +1189,7 @@ function App() {
|
|||
}} />}
|
||||
{storageError && <ConfirmModal title="Quota local atteint" message={storageError} confirmLabel="Compris" cancelLabel="Fermer" danger onClose={() => setStorageError("")} />}
|
||||
{screenshot && <ScreenshotViewer shot={screenshot} onClose={() => setScreenshot(null)} />}
|
||||
{notification && <NotificationToast key={notification.id} message={notification.message} />}
|
||||
</Shell>
|
||||
);
|
||||
}
|
||||
|
|
@ -1229,13 +1304,17 @@ function AboutPage({ siteContent }) {
|
|||
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;
|
||||
useEffect(() => {
|
||||
if (!location.hash.includes("#contribuer")) return;
|
||||
requestAnimationFrame(() => document.querySelector("#contribuer")?.scrollIntoView({ behavior: "smooth", block: "start" }));
|
||||
}, []);
|
||||
return (
|
||||
<div className="about-page">
|
||||
<section className="page-hero">
|
||||
<div>
|
||||
<p className="eyebrow">{content.eyebrow}</p>
|
||||
<h1>{content.title}</h1>
|
||||
<p>{content.description}</p>
|
||||
<p><RichText text={content.description} /></p>
|
||||
</div>
|
||||
</section>
|
||||
<section className="about-story-section nebula-panel" aria-labelledby="about-story-title">
|
||||
|
|
@ -1249,7 +1328,7 @@ function AboutPage({ siteContent }) {
|
|||
<span>{String(index + 1).padStart(2, "0")}</span>
|
||||
<div>
|
||||
<h3>{section.title}</h3>
|
||||
<p>{section.text}</p>
|
||||
<p><RichText text={section.text} /></p>
|
||||
{index === storageSectionIndex && limits.length > 0 && (
|
||||
<div className="about-storage-reminders">
|
||||
<p className="eyebrow">{content.limitsTitle}</p>
|
||||
|
|
@ -1263,7 +1342,7 @@ function AboutPage({ siteContent }) {
|
|||
</span>
|
||||
<div>
|
||||
<h4>{reminder.title}</h4>
|
||||
<p>{reminder.text}</p>
|
||||
<p><RichText text={reminder.text} /></p>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
|
|
@ -1291,12 +1370,29 @@ function AboutPage({ siteContent }) {
|
|||
</span>
|
||||
<div>
|
||||
<h3>{tool.name}</h3>
|
||||
<p>{tool.description}</p>
|
||||
<p><RichText text={tool.description} /></p>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
{content.contribute && (
|
||||
<section className="about-contribute-section nebula-panel" id="contribuer" aria-labelledby="about-contribute-title">
|
||||
<div className="about-section-heading">
|
||||
<p className="eyebrow">{content.contribute.eyebrow}</p>
|
||||
<h2 id="about-contribute-title">{content.contribute.title}</h2>
|
||||
</div>
|
||||
<div className="about-contribute-content">
|
||||
<p><RichText text={content.contribute.description} /></p>
|
||||
<pre><code>{content.contribute.example}</code></pre>
|
||||
<div className="about-prompt-box">
|
||||
<h3>{content.contribute.promptTitle}</h3>
|
||||
<p><RichText text={content.contribute.promptIntro} /></p>
|
||||
<pre><code>{content.contribute.promptText}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1544,6 +1640,7 @@ function ToolboxView({ siteContent, toolbox, toolboxGame, embedded, actions, sto
|
|||
normalizeUrl,
|
||||
hostnameFromUrl,
|
||||
copyText,
|
||||
notify: actions.notify,
|
||||
compressImageFile: compressImage,
|
||||
clampQty,
|
||||
uid,
|
||||
|
|
@ -1754,6 +1851,14 @@ function ConfirmModal({ title, message, confirmLabel = "Confirmer", cancelLabel
|
|||
);
|
||||
}
|
||||
|
||||
function NotificationToast({ message }) {
|
||||
return (
|
||||
<div className="notification-toast" role="status" aria-live="polite">
|
||||
{message}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CreateToolboxModal({ gameId, initialName = "", onClose }) {
|
||||
useModalScrollLock();
|
||||
const [name, setName] = useState(initialName);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue