+ {ungrouped.length ?
{ungrouped.map(renderTrait)}
: null}
+ {categories.map((category) => {
+ const categoryTraits = traits.filter((trait) => trait.category === category);
+ const collapsed = collapsedCategories.includes(category);
+ const className = [
+ "equipment-planner-trait-category",
+ collapsed ? "is-collapsed" : "",
+ reorder.isGroupDragging(category) ? "is-dragging" : "",
+ reorder.isGroupDropTarget(category) ? "is-drop-target" : ""
+ ].filter(Boolean).join(" ");
+ return (
+
+
+
+ {category}
+ {categoryTraits.length}
+
+
+ {!collapsed && {categoryTraits.map(renderTrait)}
}
+
+ );
+ })}
+ {!traits.length &&
{textContent.emptyCharacteristics || "Aucune caractéristique."}
}
+
+ );
+}
+
+function EquipmentTraitRow({ trait, className, itemProps, editing, textContent, onStartDrag, onUpdateTrait }) {
+ const categoryEdit = useInlineEdit({
+ value: trait.category || "",
+ onCommit: (category) => onUpdateTrait(trait.id, (current) => ({ ...current, category }))
+ });
+ const nameEdit = useInlineEdit({
+ value: trait.name,
+ transform: (draft) => String(draft || "").trim() || trait.name,
+ onCommit: (name) => onUpdateTrait(trait.id, (current) => ({ ...current, name }))
+ });
+ const valueEdit = useInlineEdit({
+ value: formatValue(trait.value),
+ onCommit: (value) => onUpdateTrait(trait.id, (current) => ({ ...current, value }))
+ });
+
+ return (
+ reorder.itemReorder.startDrag(event, trait.id)}
- onUpdateTrait={onUpdateTrait}
- />
- );
- }
-
- return (
-
- {ungrouped.length ?
{ungrouped.map(renderTrait)}
: null}
- {categories.map((category) => {
- const categoryTraits = traits.filter((trait) => trait.category === category);
- const collapsed = collapsedCategories.includes(category);
- const className = [
- "equipment-planner-trait-category",
- collapsed ? "is-collapsed" : "",
- reorder.isGroupDragging(category) ? "is-dragging" : "",
- reorder.isGroupDropTarget(category) ? "is-drop-target" : ""
- ].filter(Boolean).join(" ");
- return (
-
-
-
- {category}
- {categoryTraits.length}
-
-
- {!collapsed && {categoryTraits.map(renderTrait)}
}
-
- );
- })}
- {!traits.length &&
{textContent.emptyCharacteristics || "Aucune caractéristique."}
}
-
- );
-}
-
-function EquipmentTraitRow({ trait, className, itemProps, editing, textContent, onStartDrag, onUpdateTrait }) {
- const categoryEdit = useInlineEdit({
- value: trait.category || "",
- onCommit: (category) => onUpdateTrait(trait.id, (current) => ({ ...current, category }))
- });
- const nameEdit = useInlineEdit({
- value: trait.name,
- transform: (draft) => String(draft || "").trim() || trait.name,
- onCommit: (name) => onUpdateTrait(trait.id, (current) => ({ ...current, name }))
- });
- const valueEdit = useInlineEdit({
- value: formatValue(trait.value),
- onCommit: (value) => onUpdateTrait(trait.id, (current) => ({ ...current, value }))
- });
-
- return (
-
- {editing ? (
- <>
-
- ({ icon, label: icon }))}
- onChange={(icon) => onUpdateTrait(trait.id, (current) => ({ ...current, icon }))}
- />
-
-
-
-
- >
- ) : (
-
-
- {trait.name}
- {trait.value !== "" && {formatValue(trait.value)}}
-
- )}
-
- );
-}
-
-export function MaterialsEditor({ equipment, textContent, editing, onUpdate }) {
- const [draft, setDraft] = useState({ name: "", qty: 1 });
-
- function addMaterial(event) {
- event.preventDefault();
- const cleanName = draft.name.trim();
- if (!cleanName) return;
- onUpdate((current) => ({
- ...current,
- materials: [...current.materials, { id: createLocalId("material"), name: cleanName, qty: Math.max(1, Number.parseInt(draft.qty, 10) || 1) }]
- }));
- setDraft({ name: "", qty: 1 });
- }
-
- return (
-
- {textContent.materialsTitle || "Matériaux"}
- {editing && (
-
- )}
-
- {equipment.materials.map((material) => (
-
- ))}
-
-
- );
-}
-
-function MaterialRow({ material, editing, textContent, onUpdate }) {
- const nameEdit = useInlineEdit({
- value: material.name,
- transform: (draft) => String(draft || "").trim() || material.name,
- onCommit: (name) => onUpdate((current) => ({ ...current, materials: current.materials.map((item) => item.id === material.id ? { ...item, name } : item) }))
- });
-
- return (
-
- {editing ? (
- <>
-
- onUpdate((current) => ({ ...current, materials: current.materials.map((item) => item.id === material.id ? { ...item, qty: Math.max(1, Number.parseInt(event.target.value, 10) || 1) } : item) }))} />
-
- >
- ) : (
-
- {material.name}
- x{material.qty}
-
- )}
-
- );
-}
-
-export function SocketItemsEditor({ equipment, textContent, editing, onUpdate }) {
- const [draft, setDraft] = useState({ name: "", shape: "jewel", color: "yellow" });
-
- function addSocketItem(event) {
- event.preventDefault();
- const cleanName = draft.name.trim();
- if (!cleanName) return;
- onUpdate((current) => ({
- ...current,
- socketItems: [...current.socketItems, { id: createLocalId("socket"), name: cleanName, shape: draft.shape, color: draft.color, bonuses: [] }]
- }));
- setDraft({ ...draft, name: "" });
- }
-
- function updateSocketItem(socketItemId, updater) {
- onUpdate((current) => ({
- ...current,
- socketItems: current.socketItems.map((socketItem) => socketItem.id === socketItemId ? updater(socketItem) : socketItem).filter((socketItem) => socketItem.name)
- }));
- }
-
- function deleteSocketItem(socketItemId) {
- onUpdate((current) => ({
- ...current,
- socketItems: current.socketItems.filter((socketItem) => socketItem.id !== socketItemId),
- socketLinks: current.socketLinks.filter((link) => link.fromSocketItemId !== socketItemId && link.toSocketItemId !== socketItemId)
- }));
- }
-
- function addSocketLink(fromSocketItemId, toSocketItemId) {
- if (!toSocketItemId || fromSocketItemId === toSocketItemId) return;
- const pair = [fromSocketItemId, toSocketItemId].sort().join(":");
- if (equipment.socketLinks.some((link) => [link.fromSocketItemId, link.toSocketItemId].sort().join(":") === pair)) return;
- onUpdate((current) => ({
- ...current,
- socketLinks: [...current.socketLinks, { id: createLocalId("socket-link"), fromSocketItemId, toSocketItemId }]
- }));
- }
-
- return (
-
- {textContent.socketItemsTitle || "Objets sertis"}
- {editing && (
-
- )}
-
- {equipment.socketItems.map((socketItem) => (
- updateSocketItem(socketItem.id, updater)}
- onDelete={() => deleteSocketItem(socketItem.id)}
- onAddLink={(toSocketItemId) => addSocketLink(socketItem.id, toSocketItemId)}
- onDeleteLink={(linkId) => onUpdate((current) => ({ ...current, socketLinks: current.socketLinks.filter((link) => link.id !== linkId) }))}
- />
- ))}
-
-
- );
-}
-
-function SocketItemEditor({ equipment, socketItem, textContent, editing, onUpdate, onDelete, onAddLink, onDeleteLink }) {
- const [bonusDraft, setBonusDraft] = useState({ name: "", value: "" });
- const links = equipment.socketLinks.filter((link) => link.fromSocketItemId === socketItem.id || link.toSocketItemId === socketItem.id);
- const nameEdit = useInlineEdit({
- value: socketItem.name,
- transform: (draft) => String(draft || "").trim() || socketItem.name,
- onCommit: (name) => onUpdate((current) => ({ ...current, name }))
- });
-
- function addBonus(event) {
- event.preventDefault();
- const cleanName = bonusDraft.name.trim();
- if (!cleanName) return;
- onUpdate((current) => ({
- ...current,
- bonuses: [...current.bonuses, { id: createLocalId("bonus"), category: "", name: cleanName, value: bonusDraft.value.trim() }]
- }));
- setBonusDraft({ name: "", value: "" });
- }
-
- return (
-
-
-
- {editing ? (
- <>
-
-
- >
- ) : (
- {socketItem.name}
- )}
-
- {editing && (
- <>
-
- onUpdate((current) => ({ ...current, shape }))} />
- onUpdate((current) => ({ ...current, color }))} />
- {equipment.socketItems.length > 1 && (
-
- )}
-
-
- >
- )}
-
- {socketItem.bonuses.map((bonus) => (
- -
- {bonus.category ? `${bonus.category} · ` : ""}{bonus.name}
- {formatValue(bonus.value)}
- {editing && (
-
- )}
-
- ))}
-
- {links.length ? (
-
- {links.map((link) => {
- const otherSocketItemId = link.fromSocketItemId === socketItem.id ? link.toSocketItemId : link.fromSocketItemId;
- return (
-
- );
- })}
-
- ) : null}
-
- );
-}
diff --git a/website/src/features/toolboxes/modules/EquipmentPlannerMaterials.jsx b/website/src/features/toolboxes/modules/EquipmentPlannerMaterials.jsx
new file mode 100644
index 0000000..a56a1e5
--- /dev/null
+++ b/website/src/features/toolboxes/modules/EquipmentPlannerMaterials.jsx
@@ -0,0 +1,71 @@
+// Rôle : rend l'éditeur des matériaux de craft d'un équipement.
+import { useState } from "react";
+import { Icon } from "../../../components/Icon.jsx";
+import { useInlineEdit } from "../../../hooks/useInlineEdit.js";
+import { createLocalId } from "./equipmentPlannerOptions.js";
+
+export function MaterialsEditor({ equipment, textContent, editing, onUpdate }) {
+ const [draft, setDraft] = useState({ name: "", qty: 1 });
+
+ function addMaterial(event) {
+ event.preventDefault();
+ const cleanName = draft.name.trim();
+ if (!cleanName) return;
+ onUpdate((current) => ({
+ ...current,
+ materials: [...current.materials, { id: createLocalId("material"), name: cleanName, qty: Math.max(1, Number.parseInt(draft.qty, 10) || 1) }]
+ }));
+ setDraft({ name: "", qty: 1 });
+ }
+
+ return (
+
+ {textContent.materialsTitle || "Matériaux"}
+ {editing && (
+
+ )}
+
+ {equipment.materials.map((material) => (
+
+ ))}
+
+
+ );
+}
+
+function MaterialRow({ material, editing, textContent, onUpdate }) {
+ const nameEdit = useInlineEdit({
+ value: material.name,
+ transform: (draft) => String(draft || "").trim() || material.name,
+ onCommit: (name) => onUpdate((current) => ({ ...current, materials: current.materials.map((item) => item.id === material.id ? { ...item, name } : item) }))
+ });
+
+ return (
+
+ {editing ? (
+ <>
+
+ onUpdate((current) => ({ ...current, materials: current.materials.map((item) => item.id === material.id ? { ...item, qty: Math.max(1, Number.parseInt(event.target.value, 10) || 1) } : item) }))} />
+
+ >
+ ) : (
+
+ {material.name}
+ x{material.qty}
+
+ )}
+
+ );
+}
diff --git a/website/src/features/toolboxes/modules/EquipmentPlannerSockets.jsx b/website/src/features/toolboxes/modules/EquipmentPlannerSockets.jsx
new file mode 100644
index 0000000..1b35b21
--- /dev/null
+++ b/website/src/features/toolboxes/modules/EquipmentPlannerSockets.jsx
@@ -0,0 +1,163 @@
+// Rôle : rend l'éditeur des objets sertis, bonus et liens de sockets.
+import { useState } from "react";
+import { Icon } from "../../../components/Icon.jsx";
+import { useInlineEdit } from "../../../hooks/useInlineEdit.js";
+import {
+ createLocalId,
+ formatValue,
+ getSocketItemName
+} from "./equipmentPlannerOptions.js";
+import { ColorChoiceDropdown, ShapeChoiceDropdown, SocketShapeIcon } from "./EquipmentPlannerPickers.jsx";
+
+export function SocketItemsEditor({ equipment, textContent, editing, onUpdate }) {
+ const [draft, setDraft] = useState({ name: "", shape: "jewel", color: "yellow" });
+
+ function addSocketItem(event) {
+ event.preventDefault();
+ const cleanName = draft.name.trim();
+ if (!cleanName) return;
+ onUpdate((current) => ({
+ ...current,
+ socketItems: [...current.socketItems, { id: createLocalId("socket"), name: cleanName, shape: draft.shape, color: draft.color, bonuses: [] }]
+ }));
+ setDraft({ ...draft, name: "" });
+ }
+
+ function updateSocketItem(socketItemId, updater) {
+ onUpdate((current) => ({
+ ...current,
+ socketItems: current.socketItems.map((socketItem) => socketItem.id === socketItemId ? updater(socketItem) : socketItem).filter((socketItem) => socketItem.name)
+ }));
+ }
+
+ function deleteSocketItem(socketItemId) {
+ onUpdate((current) => ({
+ ...current,
+ socketItems: current.socketItems.filter((socketItem) => socketItem.id !== socketItemId),
+ socketLinks: current.socketLinks.filter((link) => link.fromSocketItemId !== socketItemId && link.toSocketItemId !== socketItemId)
+ }));
+ }
+
+ function addSocketLink(fromSocketItemId, toSocketItemId) {
+ if (!toSocketItemId || fromSocketItemId === toSocketItemId) return;
+ const pair = [fromSocketItemId, toSocketItemId].sort().join(":");
+ if (equipment.socketLinks.some((link) => [link.fromSocketItemId, link.toSocketItemId].sort().join(":") === pair)) return;
+ onUpdate((current) => ({
+ ...current,
+ socketLinks: [...current.socketLinks, { id: createLocalId("socket-link"), fromSocketItemId, toSocketItemId }]
+ }));
+ }
+
+ return (
+
+ {textContent.socketItemsTitle || "Objets sertis"}
+ {editing && (
+
+ )}
+
+ {equipment.socketItems.map((socketItem) => (
+ updateSocketItem(socketItem.id, updater)}
+ onDelete={() => deleteSocketItem(socketItem.id)}
+ onAddLink={(toSocketItemId) => addSocketLink(socketItem.id, toSocketItemId)}
+ onDeleteLink={(linkId) => onUpdate((current) => ({ ...current, socketLinks: current.socketLinks.filter((link) => link.id !== linkId) }))}
+ />
+ ))}
+
+
+ );
+}
+
+function SocketItemEditor({ equipment, socketItem, textContent, editing, onUpdate, onDelete, onAddLink, onDeleteLink }) {
+ const [bonusDraft, setBonusDraft] = useState({ name: "", value: "" });
+ const links = equipment.socketLinks.filter((link) => link.fromSocketItemId === socketItem.id || link.toSocketItemId === socketItem.id);
+ const nameEdit = useInlineEdit({
+ value: socketItem.name,
+ transform: (draft) => String(draft || "").trim() || socketItem.name,
+ onCommit: (name) => onUpdate((current) => ({ ...current, name }))
+ });
+
+ function addBonus(event) {
+ event.preventDefault();
+ const cleanName = bonusDraft.name.trim();
+ if (!cleanName) return;
+ onUpdate((current) => ({
+ ...current,
+ bonuses: [...current.bonuses, { id: createLocalId("bonus"), category: "", name: cleanName, value: bonusDraft.value.trim() }]
+ }));
+ setBonusDraft({ name: "", value: "" });
+ }
+
+ return (
+
+
+
+ {editing ? (
+ <>
+
+
+ >
+ ) : (
+ {socketItem.name}
+ )}
+
+ {editing && (
+ <>
+
+ onUpdate((current) => ({ ...current, shape }))} />
+ onUpdate((current) => ({ ...current, color }))} />
+ {equipment.socketItems.length > 1 && (
+
+ )}
+
+
+ >
+ )}
+
+ {socketItem.bonuses.map((bonus) => (
+ -
+ {bonus.category ? `${bonus.category} · ` : ""}{bonus.name}
+ {formatValue(bonus.value)}
+ {editing && (
+
+ )}
+
+ ))}
+
+ {links.length ? (
+
+ {links.map((link) => {
+ const otherSocketItemId = link.fromSocketItemId === socketItem.id ? link.toSocketItemId : link.fromSocketItemId;
+ return (
+
+ );
+ })}
+
+ ) : null}
+
+ );
+}