sokko-g/website/src/features/toolboxes/modules/LinksModule.jsx
Shinuwa 8eb622e05d
All checks were successful
Deploy Sokko G / deploy (push) Successful in 8s
Improve toolbox reorder and checklist editing UX
2026-08-02 22:12:33 +02:00

143 lines
5.6 KiB
JavaScript

// Rôle : fournit l'outil liens avec ajout manuel et import texte.
import { useCallback, useState } from "react";
import { Icon } from "../../../components/Icon.jsx";
import { useDraftForm } from "../../../hooks/useDraftForm.js";
import { moveItem, useGroupedReorder } from "../../../hooks/useGroupedReorder.js";
import { TextImportModal } from "./TextImportModal.jsx";
import { parseColonImportLines } from "./textImport.js";
export function LinksModule({ toolboxId, moduleId, context, editing }) {
const data = context.normalizeLinksData(context.getModuleData(toolboxId, moduleId, { links: [] }));
const textContent = context.moduleText?.links || {};
const linkDraft = useDraftForm({ title: "", url: "" });
const [textImport, setTextImport] = useState("");
const [importOpen, setImportOpen] = useState(false);
const [copiedId, setCopiedId] = useState("");
const closeImportModal = useCallback(() => setImportOpen(false), []);
const {
itemReorder,
getItemProps,
isItemDragging,
isItemDropTarget,
getDropPlacement
} = useGroupedReorder({
namespace: "links",
items: data.links,
getItemId: (link) => link.id,
getParentId: () => moduleId,
onItemMove: (operation) => save(moveItem(data.links, operation.sourceId, operation.targetId, operation.placement)),
hierarchy: { enabled: true, stickyParents: true }
});
function save(links) {
context.setModuleData(toolboxId, moduleId, { links });
}
function addLink(event) {
linkDraft.handleSubmit(event, (draft, { reset }) => {
const cleanUrl = context.normalizeUrl(draft.url);
if (!cleanUrl) return;
save([
...data.links,
{
id: context.uid("link"),
title: draft.title.trim(),
url: cleanUrl
}
]);
reset();
});
}
function importLinks(event) {
event.preventDefault();
const imported = parseColonImportLines(textImport)
.map((entry) => ({
id: context.uid("link"),
title: entry.label,
url: context.normalizeUrl(entry.value)
}))
.filter((link) => link.url);
if (!imported.length) return;
save([...data.links, ...imported]);
setTextImport("");
setImportOpen(false);
context.notify?.("Import de liens terminé.");
}
async function copyUrl(link) {
const copied = await context.copyText(link.url);
if (!copied) return;
setCopiedId(link.id);
context.notify?.(textContent.copiedTitle || "Copié");
window.setTimeout(() => setCopiedId(""), 1400);
}
return (
<>
{editing && (
<div className="module-add-panel">
<form className="inline-form links-add-form" onSubmit={addLink}>
<input {...linkDraft.getFieldProps("title", { placeholder: textContent.titlePlaceholder || "Nom du lien" })} />
<input {...linkDraft.getFieldProps("url", { placeholder: textContent.urlPlaceholder || "https://..." })} />
<button className="primary">{textContent.addButton || "Ajouter"}</button>
</form>
<div className="text-import-actions">
<button type="button" onClick={() => setImportOpen(true)}>
<Icon name="import" />
<span>{textContent.importOpenButton || "Importer du texte"}</span>
</button>
</div>
</div>
)}
{importOpen && (
<TextImportModal
title={textContent.importModalTitle || "Importer des liens"}
value={textImport}
onChange={setTextImport}
placeholder={textContent.importPlaceholder || "Importer plusieurs liens...\nBuild:https://example.com/build?class=rogue\nMap:https://example.com/map"}
submitLabel={textContent.importButton || "Importer le texte"}
onSubmit={importLinks}
onClose={closeImportModal}
/>
)}
<ul className="links-list">
{data.links.map((link) => {
const className = [
"link-item",
isItemDragging(link.id) ? "is-dragging" : "",
isItemDropTarget(link.id) ? "is-drop-target" : "",
getDropPlacement("item", link.id) === "after" ? "drop-after" : ""
].filter(Boolean).join(" ");
return (
<li className={className} key={link.id} {...getItemProps({ itemId: link.id, parentId: moduleId })}>
<button
className="link-drag-handle"
type="button"
onPointerDown={(event) => itemReorder.startDrag(event, link.id)}
aria-label={`${textContent.reorderTitle || "Déplacer"} ${link.title || link.url}`}
title={textContent.reorderTitle || "Déplacer"}
>
<Icon name="drag" />
</button>
<a href={link.url} target="_blank" rel="noreferrer" title={link.url}>
<strong>{link.title || context.hostnameFromUrl(link.url)}</strong>
<span>{link.url}</span>
</a>
<div>
<button type="button" onClick={() => copyUrl(link)} aria-label={`${textContent.copyTitle || "Copier"} ${link.title || link.url}`} title={copiedId === link.id ? textContent.copiedTitle || "Copié" : textContent.copyTitle || "Copier"}>
<Icon name="copy" />
</button>
<button type="button" className="danger" onClick={() => save(data.links.filter((item) => item.id !== link.id))} aria-label={`${textContent.deleteTitle || "Supprimer"} ${link.title || link.url}`} title={textContent.deleteTitle || "Supprimer"}>
<Icon name="trash" />
</button>
</div>
</li>
);
})}
</ul>
</>
);
}