144 lines
5.3 KiB
JavaScript
144 lines
5.3 KiB
JavaScript
// Rôle : affiche une toolbox dans la liste, avec actions rapides et icône personnalisable.
|
|
import React, { useEffect, useRef, useState } from "react";
|
|
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, draggingToolboxId, dropTarget, onDragStart }) {
|
|
const gameCoverImage = getGameCardCover(game);
|
|
const coverImage = gameCoverImage || toolbox.icon || DEFAULT_TOOLBOX_ICON;
|
|
const isDragging = draggingToolboxId === toolbox.id;
|
|
const isDropTarget = dropTarget.id === toolbox.id;
|
|
const className = [
|
|
"card",
|
|
"toolbox-card",
|
|
isDragging ? "is-dragging" : "",
|
|
isDropTarget ? "is-drop-target" : "",
|
|
isDropTarget && dropTarget.placement === "after" ? "drop-after" : ""
|
|
].filter(Boolean).join(" ");
|
|
|
|
return (
|
|
<article className={className} data-toolbox-id={toolbox.id}>
|
|
<button
|
|
className="toolbox-card-drag-handle"
|
|
type="button"
|
|
onPointerDown={(event) => onDragStart(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 [open, setOpen] = useState(false);
|
|
const pickerRef = useRef(null);
|
|
const icon = normalizeToolboxIcon(toolbox.icon);
|
|
|
|
useEffect(() => {
|
|
if (!open) return undefined;
|
|
|
|
function handlePointerDown(event) {
|
|
if (!pickerRef.current?.contains(event.target)) setOpen(false);
|
|
}
|
|
|
|
function handleKeyDown(event) {
|
|
if (event.key === "Escape") setOpen(false);
|
|
}
|
|
|
|
document.addEventListener("pointerdown", handlePointerDown);
|
|
document.addEventListener("keydown", handleKeyDown);
|
|
return () => {
|
|
document.removeEventListener("pointerdown", handlePointerDown);
|
|
document.removeEventListener("keydown", handleKeyDown);
|
|
};
|
|
}, [open]);
|
|
|
|
function selectIcon(nextIcon) {
|
|
onChange(nextIcon);
|
|
setOpen(false);
|
|
}
|
|
|
|
return (
|
|
<div className="toolbox-icon-picker" ref={pickerRef}>
|
|
<button
|
|
className="toolbox-icon-picker-button"
|
|
type="button"
|
|
onClick={() => setOpen((value) => !value)}
|
|
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>
|
|
{open && (
|
|
<div className="toolbox-icon-picker-menu" aria-label="Icônes de toolbox">
|
|
{TOOLBOX_ICONS.map((path) => (
|
|
<button
|
|
key={path}
|
|
className={path === icon ? "active" : ""}
|
|
type="button"
|
|
onClick={() => selectIcon(path)}
|
|
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>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
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>
|
|
);
|
|
}
|