add multi-page notepad and memos toolbox tool
All checks were successful
Deploy Sokko G / deploy (push) Successful in 16s
All checks were successful
Deploy Sokko G / deploy (push) Successful in 16s
This commit is contained in:
parent
fcabbe8029
commit
6125336338
33 changed files with 3787 additions and 138 deletions
192
website/src/features/toolboxes/modules/MemosModule.jsx
Normal file
192
website/src/features/toolboxes/modules/MemosModule.jsx
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
// Rôle : fournit l'outil de mémos courts avec post-it, bande déroulante et tri manuel.
|
||||
import { CompactDropdown } from "../../../components/CompactDropdown.jsx";
|
||||
import { Icon } from "../../../components/Icon.jsx";
|
||||
import { moveItem, useGroupedReorder } from "../../../hooks/useGroupedReorder.js";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { TextExchangeActions } from "./TextExchangeActions.jsx";
|
||||
import { TEXT_COLORS } from "./notepadUtils.js";
|
||||
|
||||
export function MemosModule({ toolboxId, moduleId, context, editing, textExchangeOpen }) {
|
||||
const storedData = context.getModuleData(toolboxId, moduleId, { items: [] });
|
||||
const data = useMemo(() => context.normalizeMemosData(storedData), [context, storedData]);
|
||||
const textContent = context.moduleText?.memos || {};
|
||||
const tickerItems = data.tickerEnabled ? data.items : [];
|
||||
const [drafts, setDrafts] = useState({});
|
||||
const {
|
||||
itemReorder,
|
||||
getItemProps,
|
||||
isItemDragging,
|
||||
isItemDropTarget,
|
||||
getDropPlacement
|
||||
} = useGroupedReorder({
|
||||
namespace: `memos-${moduleId}`,
|
||||
items: data.items,
|
||||
getItemId: (memo) => memo.id,
|
||||
getParentId: () => moduleId,
|
||||
orientation: "horizontal",
|
||||
onItemMove: (operation) => saveItems(moveItem(data.items, operation.sourceId, operation.targetId, operation.placement)),
|
||||
hierarchy: { enabled: true, stickyParents: true }
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setDrafts(Object.fromEntries(data.items.map((item) => [item.id, item.text])));
|
||||
}, [data.items]);
|
||||
|
||||
function save(nextData) {
|
||||
context.setModuleData(toolboxId, moduleId, nextData, "memos");
|
||||
}
|
||||
|
||||
function saveItems(items) {
|
||||
save({ ...data, items });
|
||||
}
|
||||
|
||||
function addMemo() {
|
||||
const now = new Date().toISOString();
|
||||
save({
|
||||
...data,
|
||||
items: [
|
||||
{ id: context.uid("memo"), text: textContent.defaultText || "Nouveau mémo", color: "#f6c453", updatedAt: now },
|
||||
...data.items
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
function updateMemo(memoId, updater) {
|
||||
const now = new Date().toISOString();
|
||||
save({
|
||||
...data,
|
||||
items: data.items.map((item) => item.id === memoId ? { ...updater(item), updatedAt: now } : item)
|
||||
});
|
||||
}
|
||||
|
||||
function deleteMemo(memoId) {
|
||||
save({ ...data, items: data.items.filter((item) => item.id !== memoId) });
|
||||
}
|
||||
|
||||
function commitMemoText(memoId) {
|
||||
const nextText = String(drafts[memoId] || "").trim();
|
||||
if (!nextText) {
|
||||
deleteMemo(memoId);
|
||||
return;
|
||||
}
|
||||
updateMemo(memoId, (item) => ({ ...item, text: nextText }));
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="memos-module">
|
||||
{textExchangeOpen && (
|
||||
<div className="module-text-exchange-panel">
|
||||
<TextExchangeActions type="memos" data={data} context={context} textContent={textContent} onImport={(nextData) => save(nextData)} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{editing && (
|
||||
<div className="module-add-panel">
|
||||
<button className="primary memos-add-button" type="button" onClick={addMemo}>
|
||||
<Icon name="add" />
|
||||
<span>{textContent.addButton || "Ajouter"}</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="memos-toolbar">
|
||||
<button
|
||||
className={`memos-ticker-toggle ${data.tickerEnabled ? "active" : ""}`}
|
||||
type="button"
|
||||
onClick={() => save({ ...data, tickerEnabled: !data.tickerEnabled })}
|
||||
aria-pressed={data.tickerEnabled}
|
||||
title={data.tickerEnabled ? textContent.hideTickerTitle || "Masquer la bande" : textContent.showTickerTitle || "Afficher la bande"}
|
||||
>
|
||||
<Icon name={data.tickerEnabled ? "eye-open" : "eye-closed"} />
|
||||
</button>
|
||||
{tickerItems.length > 0 && (
|
||||
<div className="memos-ticker" aria-label={textContent.tickerLabel || "Mémos"}>
|
||||
<div className="memos-ticker-track">
|
||||
{[...tickerItems, ...tickerItems].map((memo, index) => (
|
||||
<span className="memos-ticker-item" key={`${memo.id}-${index}`} style={{ "--memo-color": memo.color }}>
|
||||
{memo.text}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="memos-list">
|
||||
{data.items.length ? data.items.map((memo) => {
|
||||
const className = [
|
||||
"memo-card",
|
||||
isItemDragging(memo.id) ? "is-dragging" : "",
|
||||
isItemDropTarget(memo.id) ? "is-drop-target" : "",
|
||||
getDropPlacement("item", memo.id) === "after" ? "drop-after" : ""
|
||||
].filter(Boolean).join(" ");
|
||||
|
||||
return (
|
||||
<article className={className} key={memo.id} style={{ "--memo-color": memo.color }} {...getItemProps({ itemId: memo.id, parentId: moduleId })}>
|
||||
<button
|
||||
className="memo-drag-handle"
|
||||
type="button"
|
||||
onPointerDown={(event) => itemReorder.startDrag(event, memo.id)}
|
||||
aria-label={`${textContent.reorderTitle || "Déplacer"} ${memo.text}`}
|
||||
title={textContent.reorderTitle || "Déplacer"}
|
||||
>
|
||||
<Icon name="drag" />
|
||||
</button>
|
||||
<textarea
|
||||
value={drafts[memo.id] ?? memo.text}
|
||||
rows={3}
|
||||
placeholder={textContent.placeholder || "Mémo court..."}
|
||||
onChange={(event) => setDrafts((current) => ({ ...current, [memo.id]: event.target.value }))}
|
||||
onBlur={() => commitMemoText(memo.id)}
|
||||
aria-label={textContent.editLabel || "Modifier le mémo"}
|
||||
/>
|
||||
<footer>
|
||||
<CompactDropdown
|
||||
className="memo-color-dropdown notepad-color-dropdown"
|
||||
menuClassName="memo-color-menu notepad-color-menu"
|
||||
label={textContent.colorLabel || "Couleur du mémo"}
|
||||
renderTrigger={({ toggle }) => (
|
||||
<button
|
||||
className="notepad-color-toggle"
|
||||
type="button"
|
||||
style={{ "--swatch-color": memo.color }}
|
||||
onClick={toggle}
|
||||
title={textContent.colorLabel || "Couleur du mémo"}
|
||||
aria-label={textContent.colorLabel || "Couleur du mémo"}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
{({ close }) => (
|
||||
<>
|
||||
{TEXT_COLORS.map((color) => (
|
||||
<button
|
||||
key={color}
|
||||
className={`notepad-swatch ${memo.color === color ? "active" : ""}`}
|
||||
type="button"
|
||||
style={{ "--swatch-color": color }}
|
||||
onClick={() => {
|
||||
updateMemo(memo.id, (item) => ({ ...item, color }));
|
||||
close();
|
||||
}}
|
||||
aria-label={`${textContent.colorLabel || "Couleur"} ${color}`}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</CompactDropdown>
|
||||
<button className="memo-delete-button danger" type="button" onClick={() => deleteMemo(memo.id)} aria-label={textContent.deleteTitle || "Supprimer le mémo"} title={textContent.deleteTitle || "Supprimer"}>
|
||||
<Icon name="trash" />
|
||||
</button>
|
||||
</footer>
|
||||
</article>
|
||||
);
|
||||
}) : (
|
||||
<button className="memos-empty" type="button" onClick={addMemo}>
|
||||
<Icon name="add" />
|
||||
<span>{textContent.emptyLabel || "Ajouter un premier mémo"}</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue