sokko-g/website/src/features/toolboxes/ToolboxCard.jsx
Shinuwa 1879b245fb
All checks were successful
Deploy Sokko G / deploy (push) Successful in 7s
Unify toolbox drag and drop reorder behavior
2026-08-01 20:00:47 +02:00

126 lines
4.7 KiB
JavaScript

// Rôle : affiche une toolbox dans la liste, avec actions rapides et icône personnalisable.
import React from "react";
import { CompactDropdown } from "../../components/CompactDropdown.jsx";
import { Icon } from "../../components/Icon.jsx";
import {
DEFAULT_TOOLBOX_ICON,
normalizeToolboxIcon,
TOOLBOX_ICONS
} from "./storage/toolboxStorage.js";
export function getGameCardCover(game) {
return game?.images?.cardCover || game?.image || "";
}
export function getGameCardBackground(game) {
return game?.cover || "var(--gradient-nebula)";
}
export function formatDate(value) {
return new Intl.DateTimeFormat("fr-FR", { dateStyle: "medium", timeStyle: "short" }).format(new Date(value));
}
export function ToolboxCard({ toolbox, game, actions, reorder }) {
const gameCoverImage = getGameCardCover(game);
const coverImage = gameCoverImage || toolbox.icon || DEFAULT_TOOLBOX_ICON;
const className = [
"card",
"toolbox-card",
reorder.isItemDragging(toolbox.id) ? "is-dragging" : "",
reorder.isItemDropTarget(toolbox.id) ? "is-drop-target" : "",
reorder.getDropPlacement("item", toolbox.id) === "after" ? "drop-after" : ""
].filter(Boolean).join(" ");
return (
<article className={className} {...reorder.getItemProps({ itemId: toolbox.id })}>
<button
className="toolbox-card-drag-handle"
type="button"
onPointerDown={(event) => reorder.itemReorder.startDrag(event, toolbox.id)}
aria-label={`Déplacer ${toolbox.name}`}
title="Déplacer"
>
<Icon name="drag" />
</button>
<a
className={`card-cover toolbox-card-cover-link ${gameCoverImage ? "toolbox-card-cover" : "toolbox-icon-cover"}`}
href={`#/toolbox/${toolbox.id}`}
aria-label={`Ouvrir ${toolbox.name}`}
title={`Ouvrir ${toolbox.name}`}
style={{ "--game-cover": getGameCardBackground(game), background: gameCoverImage ? "" : "var(--gradient-nebula)" }}
>
<img src={coverImage} alt={game ? game.title : `Icône de ${toolbox.name}`} loading="lazy" />
</a>
<div className="card-body">
<p className="eyebrow">{game ? game.title : "Toolbox libre"}</p>
<h2>{toolbox.name}</h2>
<small>Modifiée le {formatDate(toolbox.updatedAt)}</small>
<div className="card-actions">
<a className="button card-icon-button" href={`#/toolbox/${toolbox.id}`} aria-label={`Ouvrir ${toolbox.name}`} title="Ouvrir"><Icon name="open" /></a>
<button className="card-icon-button" onClick={() => actions.exportToolbox(toolbox.id)} aria-label={`Exporter ${toolbox.name}`} title="Exporter"><Icon name="export" /></button>
<button className="card-icon-button danger" onClick={() => actions.setConfirmModal({
title: "Supprimer la toolbox",
message: `Supprimer "${toolbox.name}" et ses données locales ?`,
confirmLabel: "Supprimer",
danger: true,
onResolve: (confirmed) => confirmed && actions.deleteToolbox(toolbox.id)
})} aria-label={`Supprimer ${toolbox.name}`} title="Supprimer"><Icon name="trash" /></button>
</div>
</div>
</article>
);
}
export function ToolboxIconPicker({ toolbox, onChange }) {
const icon = normalizeToolboxIcon(toolbox.icon);
return (
<CompactDropdown
className="toolbox-icon-picker"
menuClassName="toolbox-icon-picker-menu"
label="Icônes de toolbox"
renderTrigger={({ open, toggle }) => (
<button
className="toolbox-icon-picker-button"
type="button"
onClick={toggle}
aria-label="Changer l'icône de la toolbox"
aria-expanded={open}
title="Changer l'icône"
>
<img src={icon} alt="" aria-hidden="true" />
</button>
)}
>
{({ close }) => (
<>
{TOOLBOX_ICONS.map((path) => (
<button
key={path}
className={path === icon ? "active" : ""}
type="button"
onClick={() => {
onChange(path);
close();
}}
aria-label={`Utiliser l'icône ${path.split("/").pop().replace(".png", "")}`}
aria-pressed={path === icon}
title={path.split("/").pop().replace(".png", "")}
>
<img src={path} alt="" aria-hidden="true" loading="lazy" />
</button>
))}
</>
)}
</CompactDropdown>
);
}
export function ToolboxGameIcon({ game }) {
const gameCoverImage = getGameCardCover(game);
return (
<div className="toolbox-game-icon" title={game.title} aria-label={`Jeu associé : ${game.title}`}>
<img src={gameCoverImage} alt="" aria-hidden="true" />
</div>
);
}