Migrate to React/Vite & Sass
Some checks failed
Deploy Sokko G / deploy (push) Failing after 4s

This commit is contained in:
Shinuwa 2026-07-21 10:09:25 +02:00
parent db4b99aee3
commit 1dee8b528f
30 changed files with 3491 additions and 2444 deletions

View file

@ -0,0 +1,66 @@
import { useState } from "react";
export function ChecklistModule({ toolboxId, moduleId, context }) {
const data = context.normalizeChecklistData(context.getModuleData(toolboxId, moduleId, { items: [] }));
const [label, setLabel] = useState("");
const [qty, setQty] = useState(1);
function save(items) {
context.setModuleData(toolboxId, moduleId, { items });
}
function addItem(event) {
event.preventDefault();
const cleanLabel = label.trim();
if (!cleanLabel) return;
save([...data.items, { id: context.uid("item"), label: cleanLabel, qtyTarget: Math.max(1, Number(qty) || 1), qtyCurrent: 0 }]);
setLabel("");
setQty(1);
}
return (
<>
<form className="inline-form checklist-add-form" onSubmit={addItem}>
<input name="label" placeholder="Nouvel item" value={label} onChange={(event) => setLabel(event.target.value)} />
<input className="checklist-qty-input" name="qty" type="number" min="1" value={qty} onChange={(event) => setQty(event.target.value)} aria-label="Quantité cible" />
<button className="primary">Ajouter</button>
</form>
<ul className="checklist">
{data.items.map((item) => (
<ChecklistItem key={item.id} item={item} toolboxId={toolboxId} moduleId={moduleId} context={context} items={data.items} save={save} />
))}
</ul>
</>
);
}
function ChecklistItem({ item, context, items, save }) {
const done = context.clampQty(item.qtyCurrent, item.qtyTarget) >= item.qtyTarget;
function updateItem(updater) {
save(items.map((entry) => entry.id === item.id ? updater(entry) : entry));
}
return (
<li className={`checklist-item ${done ? "is-complete" : ""}`}>
<div className="checklist-item-main">
{item.qtyTarget === 1 ? (
<input
type="checkbox"
checked={done}
onChange={(event) => updateItem((entry) => ({ ...entry, qtyTarget: 1, qtyCurrent: event.target.checked ? 1 : 0 }))}
aria-label={`Terminer ${item.label}`}
/>
) : (
<div className="checklist-qty-controls" aria-label={`Quantité ${item.label}`}>
<button type="button" onClick={() => updateItem((entry) => ({ ...entry, qtyCurrent: context.clampQty(entry.qtyCurrent - 1, entry.qtyTarget) }))} aria-label="Retirer une quantité">-</button>
<small>{context.clampQty(item.qtyCurrent, item.qtyTarget)}/{item.qtyTarget}</small>
<button type="button" onClick={() => updateItem((entry) => ({ ...entry, qtyCurrent: context.clampQty(entry.qtyCurrent + 1, entry.qtyTarget) }))} aria-label="Ajouter une quantité">+</button>
</div>
)}
<span>{item.label}</span>
</div>
<button onClick={() => save(items.filter((entry) => entry.id !== item.id))} aria-label={`Supprimer ${item.label}`}>×</button>
</li>
);
}

View file

@ -0,0 +1,18 @@
import { useState } from "react";
export function NotepadModule({ toolboxId, moduleId, context }) {
const data = context.getModuleData(toolboxId, moduleId, { text: "" });
const [text, setText] = useState(data.text || "");
return (
<textarea
className="notepad"
value={text}
onChange={(event) => {
setText(event.target.value);
context.setModuleData(toolboxId, moduleId, { text: event.target.value });
}}
placeholder="Notes rapides..."
/>
);
}

View file

@ -0,0 +1,72 @@
import { useState } from "react";
export function ScreenshotsModule({ toolboxId, moduleId, context }) {
const data = context.getModuleData(toolboxId, moduleId, { shots: [] });
const [dragOver, setDragOver] = useState(false);
async function addFiles(files) {
if (await context.addScreenshotFiles(toolboxId, moduleId, files)) {
setDragOver(false);
}
}
return (
<>
<label
className={`dropzone ${dragOver ? "is-drag-over" : ""}`}
onDragOver={(event) => {
event.preventDefault();
event.dataTransfer.dropEffect = "copy";
setDragOver(true);
}}
onDragLeave={() => setDragOver(false)}
onDrop={(event) => {
event.preventDefault();
addFiles(event.dataTransfer.files);
}}
>
<input type="file" accept="image/*" multiple hidden onChange={(event) => addFiles(event.target.files)} />
Ajouter des screenshots
</label>
<div
className="paste-target"
contentEditable
suppressContentEditableWarning
role="textbox"
aria-label="Coller une image depuis le presse-papiers"
onFocus={(event) => {
if (event.currentTarget.textContent.trim() === "Coller une image ici") event.currentTarget.textContent = "";
}}
onBlur={(event) => {
if (!event.currentTarget.textContent.trim()) event.currentTarget.textContent = "Coller une image ici";
}}
onPaste={(event) => {
const files = [...(event.clipboardData?.items || [])]
.filter((item) => item.type.startsWith("image/"))
.map((item) => item.getAsFile())
.filter(Boolean);
if (!files.length) return;
event.preventDefault();
event.currentTarget.textContent = "Coller une image ici";
addFiles(files);
}}
>
Coller une image ici
</div>
<div className="shots">
{data.shots.map((shot) => (
<figure key={shot.id}>
<button className="shot-preview" onClick={() => context.setScreenshot(shot)} aria-label="Agrandir le screenshot">
<img src={shot.dataUrl} alt="Screenshot" />
</button>
<button className="shot-delete-button danger" onClick={() => {
context.setModuleData(toolboxId, moduleId, { shots: data.shots.filter((item) => item.id !== shot.id) });
}} aria-label="Supprimer le screenshot" title="Supprimer">
<span className="ui-icon ui-icon-trash" aria-hidden="true" />
</button>
</figure>
))}
</div>
</>
);
}

View file

@ -0,0 +1,132 @@
import { ChecklistModule } from "./ChecklistModule.jsx";
import { NotepadModule } from "./NotepadModule.jsx";
import { ScreenshotsModule } from "./ScreenshotsModule.jsx";
const MODULE_COMPONENTS = {
notepad: { label: "Bloc notes", icon: "notepad", Component: NotepadModule },
checklist: { label: "Checklist", icon: "checklist", Component: ChecklistModule },
screenshots: { label: "Screenshots", icon: "picture", Component: ScreenshotsModule }
};
export const TOOLBOX_MODULES = Object.fromEntries(Object.entries(MODULE_COMPONENTS).map(([type, module]) => [
type,
{ label: module.label, icon: module.icon }
]));
export function ToolboxModules({ toolbox, moduleColumns, context, onRename, onDelete, onMove }) {
if (moduleColumns === 1) {
return (
<section className="modules" data-toolbox-id={toolbox.id} data-layout-cols={moduleColumns}>
{toolbox.modules.map((module) => <ModuleShell key={module.id} toolbox={toolbox} module={module} context={context} onRename={onRename} onDelete={onDelete} onMove={onMove} />)}
</section>
);
}
const columns = [[], []];
toolbox.modules.forEach((module, index) => columns[index % 2].push(module));
return (
<section className="modules" data-toolbox-id={toolbox.id} data-layout-cols={moduleColumns}>
{columns.map((modules, index) => (
<div className="module-column" key={index}>
{modules.map((module) => <ModuleShell key={module.id} toolbox={toolbox} module={module} context={context} onRename={onRename} onDelete={onDelete} onMove={onMove} />)}
</div>
))}
</section>
);
}
function ModuleShell({ toolbox, module, context, onRename, onDelete, onMove }) {
const definition = MODULE_COMPONENTS[module.type] || MODULE_COMPONENTS.notepad;
const Component = definition.Component;
const label = definition.label || module.type;
function handleDragStart(event) {
if (event.target.closest("button, input, select, textarea, [contenteditable='true']")) {
event.preventDefault();
return;
}
event.currentTarget.classList.add("is-dragging");
event.dataTransfer.effectAllowed = "move";
event.dataTransfer.setData("text/plain", module.id);
}
function handleDragOver(event) {
const fromModuleId = event.dataTransfer.getData("text/plain");
if (!fromModuleId || fromModuleId === module.id) return;
event.preventDefault();
event.dataTransfer.dropEffect = "move";
event.currentTarget.classList.add("is-drop-target");
const rect = event.currentTarget.getBoundingClientRect();
event.currentTarget.classList.toggle("drop-after", event.clientY > rect.top + rect.height / 2);
}
function handleDrop(event) {
const fromModuleId = event.dataTransfer.getData("text/plain");
if (!fromModuleId || fromModuleId === module.id) return;
event.preventDefault();
const rect = event.currentTarget.getBoundingClientRect();
onMove(fromModuleId, module.id, event.clientY > rect.top + rect.height / 2 ? "after" : "before");
}
function clearDragClasses(event) {
event.currentTarget.classList.remove("is-dragging", "is-drop-target", "drop-after");
}
return (
<article
className="module"
data-toolbox-id={toolbox.id}
data-module-id={module.id}
draggable
onDragStart={handleDragStart}
onDragOver={handleDragOver}
onDragLeave={clearDragClasses}
onDrop={handleDrop}
onDragEnd={clearDragClasses}
>
<header>
<div>
<span className="module-drag-handle" aria-hidden="true" title="Déplacer l'outil" />
<span className="module-icon" aria-hidden="true">
<span className={`module-icon-svg module-icon-${definition.icon || "notepad"}`} />
</span>
<EditableModuleTitle value={module.title || label} fallback={label} onSave={(title) => onRename(module.id, title)} />
</div>
<div>
<button className="module-delete-button danger" onClick={() => onDelete(module.id)} aria-label={`Retirer ${module.title || label}`} title="Retirer">
<span className="ui-icon ui-icon-trash" aria-hidden="true" />
</button>
</div>
</header>
<Component toolboxId={toolbox.id} moduleId={module.id} context={context} />
</article>
);
}
function EditableModuleTitle({ value, fallback, onSave }) {
return (
<h2
className="module-title"
contentEditable
suppressContentEditableWarning
spellCheck="false"
title="Cliquer pour renommer"
onFocus={(event) => { event.currentTarget.dataset.previousTitle = event.currentTarget.textContent.trim(); }}
onBlur={(event) => onSave(event.currentTarget.textContent.trim() || fallback)}
onKeyDown={(event) => {
if (event.key === "Enter") {
event.preventDefault();
event.currentTarget.blur();
}
if (event.key === "Escape") {
event.preventDefault();
event.currentTarget.textContent = event.currentTarget.dataset.previousTitle || value;
event.currentTarget.blur();
}
}}
>
{value}
</h2>
);
}