Add toolbox library page with editable examples
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
8eb622e05d
commit
9b72f4934c
20 changed files with 4774 additions and 31 deletions
257
website/src/pages/LibraryPage.jsx
Normal file
257
website/src/pages/LibraryPage.jsx
Normal file
|
|
@ -0,0 +1,257 @@
|
|||
// Rôle : affiche la librairie des outils avec des exemples locaux non persistés.
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { Icon } from "../components/Icon.jsx";
|
||||
import { CalculatorModule } from "../features/toolboxes/modules/CalculatorModule.jsx";
|
||||
import { ChecklistModule } from "../features/toolboxes/modules/ChecklistModule.jsx";
|
||||
import { CombosModule } from "../features/toolboxes/modules/CombosModule.jsx";
|
||||
import { CountersModule } from "../features/toolboxes/modules/CountersModule.jsx";
|
||||
import { ImageAnnotationModule } from "../features/toolboxes/modules/ImageAnnotationModule.jsx";
|
||||
import { ImagesModule } from "../features/toolboxes/modules/ImagesModule.jsx";
|
||||
import { LinksModule } from "../features/toolboxes/modules/LinksModule.jsx";
|
||||
import { NotepadModule } from "../features/toolboxes/modules/NotepadModule.jsx";
|
||||
import { TableModule } from "../features/toolboxes/modules/TableModule.jsx";
|
||||
import { TaskPlannerModule } from "../features/toolboxes/modules/TaskPlannerModule.jsx";
|
||||
import { TimerModule } from "../features/toolboxes/modules/TimerModule.jsx";
|
||||
import {
|
||||
clampQty,
|
||||
hostnameFromUrl,
|
||||
normalizeCalculatorData,
|
||||
normalizeChecklistData,
|
||||
normalizeCombosData,
|
||||
normalizeCountersData,
|
||||
normalizeImageAnnotationData,
|
||||
normalizeLinksData,
|
||||
normalizeNotepadData,
|
||||
normalizeTableData,
|
||||
normalizeTaskPlannerData,
|
||||
normalizeTimerData,
|
||||
normalizeUrl,
|
||||
uid
|
||||
} from "../features/toolboxes/storage/toolboxStorage.js";
|
||||
|
||||
const LIBRARY_TOOLBOX_ID = "library";
|
||||
|
||||
const MODULE_COMPONENTS = {
|
||||
notepad: { label: "Bloc notes", mode: "Texte riche", icon: "notepad", Component: NotepadModule },
|
||||
checklist: { label: "Checklist", mode: "Catégories + sans catégorie", icon: "checklist", Component: ChecklistModule, scrollable: true },
|
||||
images: { label: "Images", mode: "Média", icon: "picture", Component: ImagesModule, scrollable: true },
|
||||
links: { label: "Liens", mode: "Liste simple", icon: "link", Component: LinksModule, scrollable: true },
|
||||
counters: { label: "Compteurs", mode: "Valeurs", icon: "abacus", Component: CountersModule, scrollable: true },
|
||||
combos: { label: "Combos", mode: "Catégories", icon: "controller", Component: CombosModule, scrollable: true },
|
||||
calculator: { label: "Calculateur", mode: "Arborescence", icon: "calculator", Component: CalculatorModule },
|
||||
table: { label: "Tableau", mode: "Grille + formules", icon: "table", Component: TableModule, scrollable: true },
|
||||
timer: { label: "Timer", mode: "Chrono + comptes à rebours", icon: "clock", Component: TimerModule },
|
||||
taskPlanner: { label: "Planificateur de tâches", mode: "Catégories + parent/enfant", icon: "tasklist", Component: TaskPlannerModule, scrollable: true },
|
||||
imageAnnotation: { label: "Annotation d'images", mode: "Image + marqueurs", icon: "map", Component: ImageAnnotationModule }
|
||||
};
|
||||
|
||||
function cloneData(value) {
|
||||
if (value === undefined) return {};
|
||||
if (typeof structuredClone === "function") return structuredClone(value);
|
||||
return JSON.parse(JSON.stringify(value));
|
||||
}
|
||||
|
||||
function createModuleDataFromPayload(payload) {
|
||||
return Object.fromEntries((payload?.toolbox?.modules || []).map((module) => [
|
||||
`${LIBRARY_TOOLBOX_ID}:${module.id}`,
|
||||
cloneData(payload?.modules?.[module.id] || {})
|
||||
]));
|
||||
}
|
||||
|
||||
function getLibraryAnchorId(module) {
|
||||
return `library-tool-${String(module?.id || module?.type || "outil").replace(/[^a-z0-9_-]+/gi, "-").toLowerCase()}`;
|
||||
}
|
||||
|
||||
function getCurrentLibraryAnchor() {
|
||||
const match = location.hash.match(/^#\/library#(.+)$/);
|
||||
return match ? decodeURIComponent(match[1]) : "";
|
||||
}
|
||||
|
||||
function scrollToLibraryTool(anchorId, replace = false) {
|
||||
const element = document.getElementById(anchorId);
|
||||
if (!element) return;
|
||||
if (replace) history.replaceState(null, "", `#/library#${encodeURIComponent(anchorId)}`);
|
||||
const top = element.getBoundingClientRect().top + window.scrollY - 24;
|
||||
window.scrollTo({ top: Math.max(0, top), behavior: "smooth" });
|
||||
}
|
||||
|
||||
function getToolDescription(siteContent, definition) {
|
||||
const tools = siteContent.about?.tools || [];
|
||||
return tools.find((tool) => tool.name === definition.label)?.description || "";
|
||||
}
|
||||
|
||||
export function LibraryPage({ siteContent, actions }) {
|
||||
const content = siteContent.library;
|
||||
const [libraryPayload, setLibraryPayload] = useState(null);
|
||||
const [libraryError, setLibraryError] = useState("");
|
||||
const [moduleData, setModuleData] = useState({});
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
fetch("/data/library.json")
|
||||
.then((response) => {
|
||||
if (!response.ok) throw new Error("Impossible de charger les exemples de librairie.");
|
||||
return response.json();
|
||||
})
|
||||
.then((payload) => {
|
||||
if (cancelled) return;
|
||||
setLibraryPayload(payload);
|
||||
setModuleData(createModuleDataFromPayload(payload));
|
||||
})
|
||||
.catch((error) => {
|
||||
if (!cancelled) setLibraryError(error.message);
|
||||
});
|
||||
return () => { cancelled = true; };
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!libraryPayload) return;
|
||||
const anchorId = getCurrentLibraryAnchor();
|
||||
if (!anchorId) return;
|
||||
requestAnimationFrame(() => scrollToLibraryTool(anchorId));
|
||||
}, [libraryPayload]);
|
||||
|
||||
const moduleContext = useMemo(() => ({
|
||||
getModuleData: (toolboxId, moduleId, fallback) => moduleData[`${toolboxId}:${moduleId}`] || fallback,
|
||||
setModuleData: (toolboxId, moduleId, data) => setModuleData((current) => ({ ...current, [`${toolboxId}:${moduleId}`]: data })),
|
||||
moduleText: siteContent.toolboxes.modules,
|
||||
normalizeChecklistData,
|
||||
normalizeCombosData,
|
||||
normalizeLinksData,
|
||||
normalizeCountersData,
|
||||
normalizeCalculatorData,
|
||||
normalizeImageAnnotationData,
|
||||
normalizeNotepadData,
|
||||
normalizeTableData,
|
||||
normalizeTimerData,
|
||||
normalizeTaskPlannerData,
|
||||
normalizeUrl,
|
||||
hostnameFromUrl,
|
||||
clampQty,
|
||||
uid,
|
||||
copyText: async (value) => {
|
||||
await navigator.clipboard.writeText(value);
|
||||
return true;
|
||||
},
|
||||
notify: actions.notify,
|
||||
compressImageFile: async () => "",
|
||||
addImageFiles: async () => false,
|
||||
setImage: actions.setImage,
|
||||
createImageAnnotationModule: () => {}
|
||||
}), [actions, moduleData, siteContent.toolboxes.modules]);
|
||||
|
||||
return (
|
||||
<div className="library-page">
|
||||
<section className="page-hero library-hero">
|
||||
<div>
|
||||
<p className="eyebrow">{content.eyebrow}</p>
|
||||
<h1>{content.title}</h1>
|
||||
<p>{content.description}</p>
|
||||
</div>
|
||||
</section>
|
||||
<section className="library-summary nebula-panel" aria-label={content.summaryLabel}>
|
||||
{content.summaryItems.map((item) => (
|
||||
<article key={item.title}>
|
||||
<span className="library-summary-icon" aria-hidden="true">
|
||||
<span className={`module-icon-svg module-icon-${item.icon || "toolbox"}`} />
|
||||
</span>
|
||||
<div>
|
||||
<h2>{item.title}</h2>
|
||||
<p>{item.text}</p>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</section>
|
||||
{libraryError && <section className="empty"><h2>Librairie indisponible</h2><p>{libraryError}</p></section>}
|
||||
{!libraryError && !libraryPayload && <section className="empty"><p>Chargement des exemples...</p></section>}
|
||||
{libraryPayload && (
|
||||
<>
|
||||
<nav className="library-toc nebula-panel" aria-label={content.tocLabel || "Sommaire des outils"}>
|
||||
{libraryPayload.toolbox.modules.map((module) => {
|
||||
const definition = MODULE_COMPONENTS[module.type] || MODULE_COMPONENTS.notepad;
|
||||
const title = module.title || definition.label || "Outil";
|
||||
const anchorId = getLibraryAnchorId(module);
|
||||
return (
|
||||
<a
|
||||
key={module.id}
|
||||
href={`#/library#${anchorId}`}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
scrollToLibraryTool(anchorId, true);
|
||||
}}
|
||||
>
|
||||
<span className="module-icon" aria-hidden="true">
|
||||
<span className={`module-icon-svg module-icon-${definition.icon}`} />
|
||||
</span>
|
||||
<span>{title}</span>
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
<section className="library-tools" aria-label={content.toolsLabel}>
|
||||
{libraryPayload.toolbox.modules.map((module) => (
|
||||
<LibraryToolExample
|
||||
key={module.id}
|
||||
module={module}
|
||||
sourceData={libraryPayload.modules?.[module.id] || {}}
|
||||
description={getToolDescription(siteContent, MODULE_COMPONENTS[module.type] || MODULE_COMPONENTS.notepad)}
|
||||
context={moduleContext}
|
||||
/>
|
||||
))}
|
||||
</section>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function LibraryToolExample({ module, sourceData, description, context }) {
|
||||
const definition = MODULE_COMPONENTS[module.type] || MODULE_COMPONENTS.notepad;
|
||||
const Component = definition.Component;
|
||||
const [scrollable, setScrollable] = useState(false);
|
||||
const title = module.title || definition.label || "Outil";
|
||||
|
||||
return (
|
||||
<section id={getLibraryAnchorId(module)} className="library-tool-section">
|
||||
<div className="library-tool-intro">
|
||||
<div>
|
||||
<h2>{title}</h2>
|
||||
{description && <p>{description}</p>}
|
||||
</div>
|
||||
</div>
|
||||
<article className={`module library-tool-example ${scrollable ? "is-scrollable" : ""}`}>
|
||||
<header>
|
||||
<div>
|
||||
<span className="module-icon" aria-hidden="true">
|
||||
<span className={`module-icon-svg module-icon-${definition.icon}`} />
|
||||
</span>
|
||||
<div>
|
||||
<h2>{definition.label}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div className="library-example-actions">
|
||||
{definition.scrollable && (
|
||||
<button
|
||||
className={`module-scroll-button ${scrollable ? "active" : ""}`}
|
||||
type="button"
|
||||
onClick={() => setScrollable((value) => !value)}
|
||||
aria-label={`${scrollable ? "Désactiver" : "Activer"} le scroll de ${title}`}
|
||||
aria-pressed={scrollable}
|
||||
title={scrollable ? "Désactiver le scroll" : "Activer le scroll"}
|
||||
>
|
||||
<Icon name="scrollable" />
|
||||
<i aria-hidden="true" />
|
||||
</button>
|
||||
)}
|
||||
<button className="library-reset-button" type="button" onClick={() => context.setModuleData(LIBRARY_TOOLBOX_ID, module.id, cloneData(sourceData))} aria-label={`Réinitialiser ${title}`} title="Réinitialiser l'exemple">
|
||||
<Icon name="refresh" />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<div className={`module-content ${scrollable ? "is-scrollable legacy-scrollbar" : ""}`}>
|
||||
<Component toolboxId={LIBRARY_TOOLBOX_ID} moduleId={module.id} context={context} editing={false} />
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue