Refactor equipment planner editors
All checks were successful
Deploy Sokko G / deploy (push) Successful in 15s
All checks were successful
Deploy Sokko G / deploy (push) Successful in 15s
This commit is contained in:
parent
ed710b3399
commit
9455b31438
5 changed files with 464 additions and 435 deletions
|
|
@ -0,0 +1,210 @@
|
|||
// Rôle : rend l'éditeur des caractéristiques d'un équipement avec catégories réordonnables.
|
||||
import { useState } from "react";
|
||||
import { Icon } from "../../../components/Icon.jsx";
|
||||
import { applyGroupedReorderOperation, useGroupedReorder } from "../../../hooks/useGroupedReorder.js";
|
||||
import { useInlineEdit } from "../../../hooks/useInlineEdit.js";
|
||||
import {
|
||||
TRAIT_ICONS,
|
||||
createLocalId,
|
||||
formatValue,
|
||||
getCharacteristicCategories,
|
||||
setItemCategory
|
||||
} from "./equipmentPlannerOptions.js";
|
||||
import { IconChoiceDropdown } from "./EquipmentPlannerPickers.jsx";
|
||||
|
||||
export function CharacteristicsEditor({ equipment, textContent, editing, onUpdate }) {
|
||||
const [draft, setDraft] = useState({ icon: "sword", category: "", name: "", value: "" });
|
||||
const categories = getCharacteristicCategories(equipment);
|
||||
const {
|
||||
itemReorder,
|
||||
groupReorder,
|
||||
getItemProps,
|
||||
getGroupProps,
|
||||
isItemDragging,
|
||||
isItemDropTarget,
|
||||
isGroupDragging,
|
||||
isGroupDropTarget,
|
||||
getDropPlacement
|
||||
} = useGroupedReorder({
|
||||
namespace: `equipment-characteristics-${equipment.id}`,
|
||||
items: equipment.characteristics,
|
||||
getItemId: (trait) => trait.id,
|
||||
getItemGroup: (trait) => trait.category,
|
||||
getEffectiveGroup: (trait) => trait.category,
|
||||
reorderFeatures: {
|
||||
item: { groupChange: true },
|
||||
group: { reorder: true, itemDrop: true }
|
||||
},
|
||||
onItemMove: (operation) => onUpdate((current) => applyGroupedReorderOperation(current, {
|
||||
operation,
|
||||
itemsKey: "characteristics",
|
||||
groupOrderKey: "categoryOrder",
|
||||
collapsedGroupsKey: "collapsedCategories",
|
||||
getItemGroup: (trait) => trait.category,
|
||||
setItemGroup: setItemCategory
|
||||
})),
|
||||
onGroupMove: (operation) => onUpdate((current) => applyGroupedReorderOperation(current, {
|
||||
operation,
|
||||
itemsKey: "characteristics",
|
||||
groupOrderKey: "categoryOrder",
|
||||
collapsedGroupsKey: "collapsedCategories",
|
||||
getItemGroup: (trait) => trait.category,
|
||||
setItemGroup: setItemCategory
|
||||
}))
|
||||
});
|
||||
const reorder = { itemReorder, groupReorder, getItemProps, getGroupProps, isItemDragging, isItemDropTarget, isGroupDragging, isGroupDropTarget, getDropPlacement };
|
||||
|
||||
function addTrait(event) {
|
||||
event.preventDefault();
|
||||
const cleanName = draft.name.trim();
|
||||
if (!cleanName) return;
|
||||
onUpdate((current) => ({
|
||||
...current,
|
||||
characteristics: [...current.characteristics, { id: createLocalId("trait"), icon: draft.icon, category: draft.category.trim(), name: cleanName, value: draft.value.trim() }],
|
||||
categoryOrder: draft.category.trim() && !current.categoryOrder.includes(draft.category.trim()) ? [...current.categoryOrder, draft.category.trim()] : current.categoryOrder
|
||||
}));
|
||||
setDraft({ icon: draft.icon, category: draft.category, name: "", value: "" });
|
||||
}
|
||||
|
||||
function updateTrait(traitId, updater) {
|
||||
onUpdate((current) => ({ ...current, characteristics: current.characteristics.map((trait) => trait.id === traitId ? updater(trait) : trait).filter((trait) => trait.name) }));
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="equipment-planner-panel">
|
||||
<h4>{textContent.characteristicsTitle || "Caractéristiques"}</h4>
|
||||
{editing && (
|
||||
<form className="inline-form equipment-planner-trait-form" onSubmit={addTrait}>
|
||||
<IconChoiceDropdown
|
||||
label={textContent.traitIconLabel || "Icône"}
|
||||
value={draft.icon}
|
||||
options={TRAIT_ICONS.map((icon) => ({ icon, label: icon }))}
|
||||
onChange={(icon) => setDraft((current) => ({ ...current, icon }))}
|
||||
/>
|
||||
<input value={draft.category} onChange={(event) => setDraft((current) => ({ ...current, category: event.target.value }))} placeholder={textContent.categoryPlaceholder || "Catégorie"} />
|
||||
<input value={draft.name} onChange={(event) => setDraft((current) => ({ ...current, name: event.target.value }))} placeholder={textContent.traitNamePlaceholder || "Nom"} />
|
||||
<input value={draft.value} onChange={(event) => setDraft((current) => ({ ...current, value: event.target.value }))} placeholder={textContent.traitValuePlaceholder || "Valeur"} />
|
||||
<button>{textContent.addTraitButton || "Ajouter"}</button>
|
||||
</form>
|
||||
)}
|
||||
<TraitCategoryList
|
||||
traits={equipment.characteristics}
|
||||
categories={categories}
|
||||
collapsedCategories={equipment.collapsedCategories}
|
||||
reorder={reorder}
|
||||
textContent={textContent}
|
||||
onToggleCategory={(category) => onUpdate((current) => ({
|
||||
...current,
|
||||
collapsedCategories: current.collapsedCategories.includes(category)
|
||||
? current.collapsedCategories.filter((item) => item !== category)
|
||||
: [...current.collapsedCategories, category]
|
||||
}))}
|
||||
onUpdateTrait={updateTrait}
|
||||
editing={editing}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function TraitCategoryList({ traits, categories, collapsedCategories, reorder, textContent, onToggleCategory, onUpdateTrait, editing }) {
|
||||
const ungrouped = traits.filter((trait) => !trait.category);
|
||||
|
||||
function renderTrait(trait) {
|
||||
const traitClassName = [
|
||||
"equipment-planner-trait",
|
||||
reorder.isItemDragging(trait.id) ? "is-dragging" : "",
|
||||
reorder.isItemDropTarget(trait.id) ? "is-drop-target" : "",
|
||||
reorder.getDropPlacement("item", trait.id) === "after" ? "drop-after" : ""
|
||||
].filter(Boolean).join(" ");
|
||||
return (
|
||||
<EquipmentTraitRow
|
||||
key={trait.id}
|
||||
trait={trait}
|
||||
className={traitClassName}
|
||||
itemProps={reorder.getItemProps({ itemId: trait.id, groupId: trait.category })}
|
||||
editing={editing}
|
||||
textContent={textContent}
|
||||
onStartDrag={(event) => reorder.itemReorder.startDrag(event, trait.id)}
|
||||
onUpdateTrait={onUpdateTrait}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="equipment-planner-trait-list">
|
||||
{ungrouped.length ? <ul>{ungrouped.map(renderTrait)}</ul> : 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 (
|
||||
<section className={className} key={category} {...reorder.getGroupProps({ groupId: category })}>
|
||||
<div className="equipment-planner-trait-category-header">
|
||||
<button type="button" className="task-planner-category-drag-handle" onPointerDown={(event) => reorder.groupReorder.startDrag(event, { groupId: category })} title={textContent.reorderCategoryTitle || "Déplacer la catégorie"}>
|
||||
<Icon name="drag" />
|
||||
</button>
|
||||
<strong>{category}</strong>
|
||||
<span>{categoryTraits.length}</span>
|
||||
<button type="button" className="checklist-section-collapse-button" onClick={() => onToggleCategory(category)} aria-expanded={!collapsed}>
|
||||
<Icon name={collapsed ? "chevron-down" : "chevron-up"} />
|
||||
</button>
|
||||
</div>
|
||||
{!collapsed && <ul>{categoryTraits.map(renderTrait)}</ul>}
|
||||
</section>
|
||||
);
|
||||
})}
|
||||
{!traits.length && <p className="equipment-planner-muted">{textContent.emptyCharacteristics || "Aucune caractéristique."}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<li className={className} {...itemProps}>
|
||||
{editing ? (
|
||||
<>
|
||||
<button type="button" className="task-planner-drag-handle" onPointerDown={onStartDrag} title={textContent.reorderTraitTitle || "Déplacer"}>
|
||||
<Icon name="drag" />
|
||||
</button>
|
||||
<IconChoiceDropdown
|
||||
label={textContent.traitIconLabel || "Icône"}
|
||||
value={trait.icon || "sword"}
|
||||
options={TRAIT_ICONS.map((icon) => ({ icon, label: icon }))}
|
||||
onChange={(icon) => onUpdateTrait(trait.id, (current) => ({ ...current, icon }))}
|
||||
/>
|
||||
<input {...categoryEdit.getInputProps({ placeholder: textContent.categoryPlaceholder || "Catégorie" })} />
|
||||
<input {...nameEdit.getInputProps({ placeholder: textContent.traitNamePlaceholder || "Nom" })} />
|
||||
<input {...valueEdit.getInputProps({ placeholder: textContent.traitValuePlaceholder || "Valeur" })} />
|
||||
<button type="button" className="task-planner-delete-button danger" onClick={() => onUpdateTrait(trait.id, () => ({ ...trait, name: "" }))} title={textContent.deleteTraitTitle || "Supprimer"}>
|
||||
<Icon name="trash" />
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<div className="equipment-planner-trait-readonly">
|
||||
<Icon name={trait.icon || "sword"} />
|
||||
<span>{trait.name}</span>
|
||||
{trait.value !== "" && <b>{formatValue(trait.value)}</b>}
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,17 +1,11 @@
|
|||
// Rôle : contient les panneaux d'affichage et d'édition détaillée du planificateur d'équipements.
|
||||
import { useState } from "react";
|
||||
// Rôle : expose l'affichage détaillé et les éditeurs spécialisés du planificateur d'équipements.
|
||||
import { Icon } from "../../../components/Icon.jsx";
|
||||
import { applyGroupedReorderOperation, useGroupedReorder } from "../../../hooks/useGroupedReorder.js";
|
||||
import { useInlineEdit } from "../../../hooks/useInlineEdit.js";
|
||||
import {
|
||||
TRAIT_ICONS,
|
||||
createLocalId,
|
||||
formatValue,
|
||||
getCharacteristicCategories,
|
||||
getSocketItemName,
|
||||
setItemCategory
|
||||
} from "./equipmentPlannerOptions.js";
|
||||
import { ColorChoiceDropdown, IconChoiceDropdown, ShapeChoiceDropdown, SocketShapeIcon } from "./EquipmentPlannerPickers.jsx";
|
||||
import { formatValue } from "./equipmentPlannerOptions.js";
|
||||
import { SocketShapeIcon } from "./EquipmentPlannerPickers.jsx";
|
||||
|
||||
export { CharacteristicsEditor } from "./EquipmentPlannerCharacteristics.jsx";
|
||||
export { MaterialsEditor } from "./EquipmentPlannerMaterials.jsx";
|
||||
export { SocketItemsEditor } from "./EquipmentPlannerSockets.jsx";
|
||||
|
||||
export function EquipmentDisplay({ equipment, textContent }) {
|
||||
const hasStats = equipment.characteristics.length > 0;
|
||||
|
|
@ -60,9 +54,9 @@ export function EquipmentDisplay({ equipment, textContent }) {
|
|||
<h4>{textContent.craftTab || "Craft"}</h4>
|
||||
{equipment.obtain ? <p>{equipment.obtain}</p> : null}
|
||||
{equipment.materials.length ? (
|
||||
<ul>
|
||||
{equipment.materials.map((material) => <li key={material.id}><span>{material.name}</span><b>x{material.qty}</b></li>)}
|
||||
</ul>
|
||||
<ul>
|
||||
{equipment.materials.map((material) => <li key={material.id}><span>{material.name}</span><b>x{material.qty}</b></li>)}
|
||||
</ul>
|
||||
) : null}
|
||||
</section>
|
||||
)}
|
||||
|
|
@ -75,419 +69,3 @@ export function TabIcon({ tab }) {
|
|||
if (tab === "sockets") return <Icon name="jewel" />;
|
||||
return <Icon name="hammer" />;
|
||||
}
|
||||
|
||||
export function CharacteristicsEditor({ equipment, textContent, editing, onUpdate }) {
|
||||
const [draft, setDraft] = useState({ icon: "sword", category: "", name: "", value: "" });
|
||||
const categories = getCharacteristicCategories(equipment);
|
||||
const {
|
||||
itemReorder,
|
||||
groupReorder,
|
||||
getItemProps,
|
||||
getGroupProps,
|
||||
isItemDragging,
|
||||
isItemDropTarget,
|
||||
isGroupDragging,
|
||||
isGroupDropTarget,
|
||||
getDropPlacement
|
||||
} = useGroupedReorder({
|
||||
namespace: `equipment-characteristics-${equipment.id}`,
|
||||
items: equipment.characteristics,
|
||||
getItemId: (trait) => trait.id,
|
||||
getItemGroup: (trait) => trait.category,
|
||||
getEffectiveGroup: (trait) => trait.category,
|
||||
reorderFeatures: {
|
||||
item: { groupChange: true },
|
||||
group: { reorder: true, itemDrop: true }
|
||||
},
|
||||
onItemMove: (operation) => onUpdate((current) => applyGroupedReorderOperation(current, {
|
||||
operation,
|
||||
itemsKey: "characteristics",
|
||||
groupOrderKey: "categoryOrder",
|
||||
collapsedGroupsKey: "collapsedCategories",
|
||||
getItemGroup: (trait) => trait.category,
|
||||
setItemGroup: setItemCategory
|
||||
})),
|
||||
onGroupMove: (operation) => onUpdate((current) => applyGroupedReorderOperation(current, {
|
||||
operation,
|
||||
itemsKey: "characteristics",
|
||||
groupOrderKey: "categoryOrder",
|
||||
collapsedGroupsKey: "collapsedCategories",
|
||||
getItemGroup: (trait) => trait.category,
|
||||
setItemGroup: setItemCategory
|
||||
}))
|
||||
});
|
||||
const reorder = { itemReorder, groupReorder, getItemProps, getGroupProps, isItemDragging, isItemDropTarget, isGroupDragging, isGroupDropTarget, getDropPlacement };
|
||||
|
||||
function addTrait(event) {
|
||||
event.preventDefault();
|
||||
const cleanName = draft.name.trim();
|
||||
if (!cleanName) return;
|
||||
onUpdate((current) => ({
|
||||
...current,
|
||||
characteristics: [...current.characteristics, { id: createLocalId("trait"), icon: draft.icon, category: draft.category.trim(), name: cleanName, value: draft.value.trim() }],
|
||||
categoryOrder: draft.category.trim() && !current.categoryOrder.includes(draft.category.trim()) ? [...current.categoryOrder, draft.category.trim()] : current.categoryOrder
|
||||
}));
|
||||
setDraft({ icon: draft.icon, category: draft.category, name: "", value: "" });
|
||||
}
|
||||
|
||||
function updateTrait(traitId, updater) {
|
||||
onUpdate((current) => ({ ...current, characteristics: current.characteristics.map((trait) => trait.id === traitId ? updater(trait) : trait).filter((trait) => trait.name) }));
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="equipment-planner-panel">
|
||||
<h4>{textContent.characteristicsTitle || "Caractéristiques"}</h4>
|
||||
{editing && (
|
||||
<form className="inline-form equipment-planner-trait-form" onSubmit={addTrait}>
|
||||
<IconChoiceDropdown
|
||||
label={textContent.traitIconLabel || "Icône"}
|
||||
value={draft.icon}
|
||||
options={TRAIT_ICONS.map((icon) => ({ icon, label: icon }))}
|
||||
onChange={(icon) => setDraft((current) => ({ ...current, icon }))}
|
||||
/>
|
||||
<input value={draft.category} onChange={(event) => setDraft((current) => ({ ...current, category: event.target.value }))} placeholder={textContent.categoryPlaceholder || "Catégorie"} />
|
||||
<input value={draft.name} onChange={(event) => setDraft((current) => ({ ...current, name: event.target.value }))} placeholder={textContent.traitNamePlaceholder || "Nom"} />
|
||||
<input value={draft.value} onChange={(event) => setDraft((current) => ({ ...current, value: event.target.value }))} placeholder={textContent.traitValuePlaceholder || "Valeur"} />
|
||||
<button>{textContent.addTraitButton || "Ajouter"}</button>
|
||||
</form>
|
||||
)}
|
||||
<TraitCategoryList
|
||||
traits={equipment.characteristics}
|
||||
categories={categories}
|
||||
collapsedCategories={equipment.collapsedCategories}
|
||||
reorder={reorder}
|
||||
textContent={textContent}
|
||||
onToggleCategory={(category) => onUpdate((current) => ({
|
||||
...current,
|
||||
collapsedCategories: current.collapsedCategories.includes(category)
|
||||
? current.collapsedCategories.filter((item) => item !== category)
|
||||
: [...current.collapsedCategories, category]
|
||||
}))}
|
||||
onUpdateTrait={updateTrait}
|
||||
editing={editing}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function TraitCategoryList({ traits, categories, collapsedCategories, reorder, textContent, onToggleCategory, onUpdateTrait, editing }) {
|
||||
const ungrouped = traits.filter((trait) => !trait.category);
|
||||
|
||||
function renderTrait(trait) {
|
||||
const traitClassName = [
|
||||
"equipment-planner-trait",
|
||||
reorder.isItemDragging(trait.id) ? "is-dragging" : "",
|
||||
reorder.isItemDropTarget(trait.id) ? "is-drop-target" : "",
|
||||
reorder.getDropPlacement("item", trait.id) === "after" ? "drop-after" : ""
|
||||
].filter(Boolean).join(" ");
|
||||
return (
|
||||
<EquipmentTraitRow
|
||||
key={trait.id}
|
||||
trait={trait}
|
||||
className={traitClassName}
|
||||
itemProps={reorder.getItemProps({ itemId: trait.id, groupId: trait.category })}
|
||||
editing={editing}
|
||||
textContent={textContent}
|
||||
onStartDrag={(event) => reorder.itemReorder.startDrag(event, trait.id)}
|
||||
onUpdateTrait={onUpdateTrait}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="equipment-planner-trait-list">
|
||||
{ungrouped.length ? <ul>{ungrouped.map(renderTrait)}</ul> : 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 (
|
||||
<section className={className} key={category} {...reorder.getGroupProps({ groupId: category })}>
|
||||
<div className="equipment-planner-trait-category-header">
|
||||
<button type="button" className="task-planner-category-drag-handle" onPointerDown={(event) => reorder.groupReorder.startDrag(event, { groupId: category })} title={textContent.reorderCategoryTitle || "Déplacer la catégorie"}>
|
||||
<Icon name="drag" />
|
||||
</button>
|
||||
<strong>{category}</strong>
|
||||
<span>{categoryTraits.length}</span>
|
||||
<button type="button" className="checklist-section-collapse-button" onClick={() => onToggleCategory(category)} aria-expanded={!collapsed}>
|
||||
<Icon name={collapsed ? "chevron-down" : "chevron-up"} />
|
||||
</button>
|
||||
</div>
|
||||
{!collapsed && <ul>{categoryTraits.map(renderTrait)}</ul>}
|
||||
</section>
|
||||
);
|
||||
})}
|
||||
{!traits.length && <p className="equipment-planner-muted">{textContent.emptyCharacteristics || "Aucune caractéristique."}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<li className={className} {...itemProps}>
|
||||
{editing ? (
|
||||
<>
|
||||
<button type="button" className="task-planner-drag-handle" onPointerDown={onStartDrag} title={textContent.reorderTraitTitle || "Déplacer"}>
|
||||
<Icon name="drag" />
|
||||
</button>
|
||||
<IconChoiceDropdown
|
||||
label={textContent.traitIconLabel || "Icône"}
|
||||
value={trait.icon || "sword"}
|
||||
options={TRAIT_ICONS.map((icon) => ({ icon, label: icon }))}
|
||||
onChange={(icon) => onUpdateTrait(trait.id, (current) => ({ ...current, icon }))}
|
||||
/>
|
||||
<input {...categoryEdit.getInputProps({ placeholder: textContent.categoryPlaceholder || "Catégorie" })} />
|
||||
<input {...nameEdit.getInputProps({ placeholder: textContent.traitNamePlaceholder || "Nom" })} />
|
||||
<input {...valueEdit.getInputProps({ placeholder: textContent.traitValuePlaceholder || "Valeur" })} />
|
||||
<button type="button" className="task-planner-delete-button danger" onClick={() => onUpdateTrait(trait.id, () => ({ ...trait, name: "" }))} title={textContent.deleteTraitTitle || "Supprimer"}>
|
||||
<Icon name="trash" />
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<div className="equipment-planner-trait-readonly">
|
||||
<Icon name={trait.icon || "sword"} />
|
||||
<span>{trait.name}</span>
|
||||
{trait.value !== "" && <b>{formatValue(trait.value)}</b>}
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<section className="equipment-planner-panel">
|
||||
<h4>{textContent.materialsTitle || "Matériaux"}</h4>
|
||||
{editing && (
|
||||
<form className="inline-form equipment-planner-material-form" onSubmit={addMaterial}>
|
||||
<input value={draft.name} onChange={(event) => setDraft((current) => ({ ...current, name: event.target.value }))} placeholder={textContent.materialNamePlaceholder || "Matériau"} />
|
||||
<input type="number" min="1" value={draft.qty} onChange={(event) => setDraft((current) => ({ ...current, qty: event.target.value }))} aria-label={textContent.materialQtyLabel || "Quantité"} />
|
||||
<button>{textContent.addMaterialButton || "Ajouter"}</button>
|
||||
</form>
|
||||
)}
|
||||
<ul className="equipment-planner-materials">
|
||||
{equipment.materials.map((material) => (
|
||||
<MaterialRow
|
||||
key={material.id}
|
||||
material={material}
|
||||
editing={editing}
|
||||
textContent={textContent}
|
||||
onUpdate={onUpdate}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<li>
|
||||
{editing ? (
|
||||
<>
|
||||
<input {...nameEdit.getInputProps()} />
|
||||
<input type="number" min="1" value={material.qty} onChange={(event) => 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) }))} />
|
||||
<button type="button" className="task-planner-delete-button danger" onClick={() => onUpdate((current) => ({ ...current, materials: current.materials.filter((item) => item.id !== material.id) }))} title={textContent.deleteMaterialTitle || "Supprimer"}>
|
||||
<Icon name="trash" />
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<div className="equipment-planner-material-readonly">
|
||||
<span>{material.name}</span>
|
||||
<b>x{material.qty}</b>
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<section className="equipment-planner-panel">
|
||||
<h4>{textContent.socketItemsTitle || "Objets sertis"}</h4>
|
||||
{editing && (
|
||||
<form className="inline-form equipment-planner-socket-form" onSubmit={addSocketItem}>
|
||||
<input value={draft.name} onChange={(event) => setDraft((current) => ({ ...current, name: event.target.value }))} placeholder={textContent.socketNamePlaceholder || "Joyau, materia..."} />
|
||||
<ShapeChoiceDropdown label={textContent.socketShapeLabel || "Forme"} value={draft.shape} onChange={(shape) => setDraft((current) => ({ ...current, shape }))} />
|
||||
<ColorChoiceDropdown label={textContent.socketColorLabel || "Couleur"} value={draft.color} onChange={(color) => setDraft((current) => ({ ...current, color }))} />
|
||||
<button>{textContent.addSocketButton || "Ajouter"}</button>
|
||||
</form>
|
||||
)}
|
||||
<div className="equipment-planner-socket-grid">
|
||||
{equipment.socketItems.map((socketItem) => (
|
||||
<SocketItemEditor
|
||||
key={socketItem.id}
|
||||
equipment={equipment}
|
||||
socketItem={socketItem}
|
||||
textContent={textContent}
|
||||
editing={editing}
|
||||
onUpdate={(updater) => 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) }))}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<article className="equipment-planner-socket-card">
|
||||
<div className="equipment-planner-socket-heading">
|
||||
<SocketShapeIcon shape={socketItem.shape} color={socketItem.color} />
|
||||
{editing ? (
|
||||
<>
|
||||
<input {...nameEdit.getInputProps()} />
|
||||
<button type="button" className="task-planner-delete-button danger" onClick={onDelete} title={textContent.deleteSocketTitle || "Supprimer"}>
|
||||
<Icon name="trash" />
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<strong>{socketItem.name}</strong>
|
||||
)}
|
||||
</div>
|
||||
{editing && (
|
||||
<>
|
||||
<div className="equipment-planner-socket-controls">
|
||||
<ShapeChoiceDropdown label={textContent.socketShapeLabel || "Forme"} value={socketItem.shape} onChange={(shape) => onUpdate((current) => ({ ...current, shape }))} />
|
||||
<ColorChoiceDropdown label={textContent.socketColorLabel || "Couleur"} value={socketItem.color} onChange={(color) => onUpdate((current) => ({ ...current, color }))} />
|
||||
{equipment.socketItems.length > 1 && (
|
||||
<select defaultValue="" onChange={(event) => { onAddLink(event.target.value); event.target.value = ""; }} aria-label={textContent.addSocketLinkLabel || "Lier à"}>
|
||||
<option value="">{textContent.addSocketLinkLabel || "Lier à"}</option>
|
||||
{equipment.socketItems.filter((item) => item.id !== socketItem.id).map((item) => <option key={item.id} value={item.id}>{item.name}</option>)}
|
||||
</select>
|
||||
)}
|
||||
</div>
|
||||
<form className="inline-form equipment-planner-bonus-form" onSubmit={addBonus}>
|
||||
<input value={bonusDraft.name} onChange={(event) => setBonusDraft((current) => ({ ...current, name: event.target.value }))} placeholder={textContent.bonusNamePlaceholder || "Bonus"} />
|
||||
<input value={bonusDraft.value} onChange={(event) => setBonusDraft((current) => ({ ...current, value: event.target.value }))} placeholder={textContent.traitValuePlaceholder || "Valeur"} />
|
||||
<button aria-label={textContent.addBonusButton || "Ajouter"} title={textContent.addBonusButton || "Ajouter"}>+</button>
|
||||
</form>
|
||||
</>
|
||||
)}
|
||||
<ul className="equipment-planner-socket-bonuses">
|
||||
{socketItem.bonuses.map((bonus) => (
|
||||
<li key={bonus.id}>
|
||||
<span>{bonus.category ? `${bonus.category} · ` : ""}{bonus.name}</span>
|
||||
<b>{formatValue(bonus.value)}</b>
|
||||
{editing && (
|
||||
<button type="button" className="task-planner-delete-button danger" onClick={() => onUpdate((current) => ({ ...current, bonuses: current.bonuses.filter((item) => item.id !== bonus.id) }))} title={textContent.deleteBonusTitle || "Supprimer"}>
|
||||
<Icon name="trash" />
|
||||
</button>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{links.length ? (
|
||||
<div className={`equipment-planner-socket-links ${editing ? "is-editing" : ""}`}>
|
||||
{links.map((link) => {
|
||||
const otherSocketItemId = link.fromSocketItemId === socketItem.id ? link.toSocketItemId : link.fromSocketItemId;
|
||||
return (
|
||||
<button key={link.id} type="button" className="equipment-planner-link-chip" onClick={editing ? () => onDeleteLink(link.id) : undefined} title={editing ? textContent.deleteSocketLinkTitle || "Retirer le lien" : getSocketItemName(equipment, otherSocketItemId)}>
|
||||
<span>{getSocketItemName(equipment, otherSocketItemId)}</span>
|
||||
{editing && <Icon name="close" />}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<section className="equipment-planner-panel">
|
||||
<h4>{textContent.materialsTitle || "Matériaux"}</h4>
|
||||
{editing && (
|
||||
<form className="inline-form equipment-planner-material-form" onSubmit={addMaterial}>
|
||||
<input value={draft.name} onChange={(event) => setDraft((current) => ({ ...current, name: event.target.value }))} placeholder={textContent.materialNamePlaceholder || "Matériau"} />
|
||||
<input type="number" min="1" value={draft.qty} onChange={(event) => setDraft((current) => ({ ...current, qty: event.target.value }))} aria-label={textContent.materialQtyLabel || "Quantité"} />
|
||||
<button>{textContent.addMaterialButton || "Ajouter"}</button>
|
||||
</form>
|
||||
)}
|
||||
<ul className="equipment-planner-materials">
|
||||
{equipment.materials.map((material) => (
|
||||
<MaterialRow
|
||||
key={material.id}
|
||||
material={material}
|
||||
editing={editing}
|
||||
textContent={textContent}
|
||||
onUpdate={onUpdate}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<li>
|
||||
{editing ? (
|
||||
<>
|
||||
<input {...nameEdit.getInputProps()} />
|
||||
<input type="number" min="1" value={material.qty} onChange={(event) => 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) }))} />
|
||||
<button type="button" className="task-planner-delete-button danger" onClick={() => onUpdate((current) => ({ ...current, materials: current.materials.filter((item) => item.id !== material.id) }))} title={textContent.deleteMaterialTitle || "Supprimer"}>
|
||||
<Icon name="trash" />
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<div className="equipment-planner-material-readonly">
|
||||
<span>{material.name}</span>
|
||||
<b>x{material.qty}</b>
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<section className="equipment-planner-panel">
|
||||
<h4>{textContent.socketItemsTitle || "Objets sertis"}</h4>
|
||||
{editing && (
|
||||
<form className="inline-form equipment-planner-socket-form" onSubmit={addSocketItem}>
|
||||
<input value={draft.name} onChange={(event) => setDraft((current) => ({ ...current, name: event.target.value }))} placeholder={textContent.socketNamePlaceholder || "Joyau, materia..."} />
|
||||
<ShapeChoiceDropdown label={textContent.socketShapeLabel || "Forme"} value={draft.shape} onChange={(shape) => setDraft((current) => ({ ...current, shape }))} />
|
||||
<ColorChoiceDropdown label={textContent.socketColorLabel || "Couleur"} value={draft.color} onChange={(color) => setDraft((current) => ({ ...current, color }))} />
|
||||
<button>{textContent.addSocketButton || "Ajouter"}</button>
|
||||
</form>
|
||||
)}
|
||||
<div className="equipment-planner-socket-grid">
|
||||
{equipment.socketItems.map((socketItem) => (
|
||||
<SocketItemEditor
|
||||
key={socketItem.id}
|
||||
equipment={equipment}
|
||||
socketItem={socketItem}
|
||||
textContent={textContent}
|
||||
editing={editing}
|
||||
onUpdate={(updater) => 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) }))}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<article className="equipment-planner-socket-card">
|
||||
<div className="equipment-planner-socket-heading">
|
||||
<SocketShapeIcon shape={socketItem.shape} color={socketItem.color} />
|
||||
{editing ? (
|
||||
<>
|
||||
<input {...nameEdit.getInputProps()} />
|
||||
<button type="button" className="task-planner-delete-button danger" onClick={onDelete} title={textContent.deleteSocketTitle || "Supprimer"}>
|
||||
<Icon name="trash" />
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<strong>{socketItem.name}</strong>
|
||||
)}
|
||||
</div>
|
||||
{editing && (
|
||||
<>
|
||||
<div className="equipment-planner-socket-controls">
|
||||
<ShapeChoiceDropdown label={textContent.socketShapeLabel || "Forme"} value={socketItem.shape} onChange={(shape) => onUpdate((current) => ({ ...current, shape }))} />
|
||||
<ColorChoiceDropdown label={textContent.socketColorLabel || "Couleur"} value={socketItem.color} onChange={(color) => onUpdate((current) => ({ ...current, color }))} />
|
||||
{equipment.socketItems.length > 1 && (
|
||||
<select defaultValue="" onChange={(event) => { onAddLink(event.target.value); event.target.value = ""; }} aria-label={textContent.addSocketLinkLabel || "Lier à"}>
|
||||
<option value="">{textContent.addSocketLinkLabel || "Lier à"}</option>
|
||||
{equipment.socketItems.filter((item) => item.id !== socketItem.id).map((item) => <option key={item.id} value={item.id}>{item.name}</option>)}
|
||||
</select>
|
||||
)}
|
||||
</div>
|
||||
<form className="inline-form equipment-planner-bonus-form" onSubmit={addBonus}>
|
||||
<input value={bonusDraft.name} onChange={(event) => setBonusDraft((current) => ({ ...current, name: event.target.value }))} placeholder={textContent.bonusNamePlaceholder || "Bonus"} />
|
||||
<input value={bonusDraft.value} onChange={(event) => setBonusDraft((current) => ({ ...current, value: event.target.value }))} placeholder={textContent.traitValuePlaceholder || "Valeur"} />
|
||||
<button aria-label={textContent.addBonusButton || "Ajouter"} title={textContent.addBonusButton || "Ajouter"}>+</button>
|
||||
</form>
|
||||
</>
|
||||
)}
|
||||
<ul className="equipment-planner-socket-bonuses">
|
||||
{socketItem.bonuses.map((bonus) => (
|
||||
<li key={bonus.id}>
|
||||
<span>{bonus.category ? `${bonus.category} · ` : ""}{bonus.name}</span>
|
||||
<b>{formatValue(bonus.value)}</b>
|
||||
{editing && (
|
||||
<button type="button" className="task-planner-delete-button danger" onClick={() => onUpdate((current) => ({ ...current, bonuses: current.bonuses.filter((item) => item.id !== bonus.id) }))} title={textContent.deleteBonusTitle || "Supprimer"}>
|
||||
<Icon name="trash" />
|
||||
</button>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{links.length ? (
|
||||
<div className={`equipment-planner-socket-links ${editing ? "is-editing" : ""}`}>
|
||||
{links.map((link) => {
|
||||
const otherSocketItemId = link.fromSocketItemId === socketItem.id ? link.toSocketItemId : link.fromSocketItemId;
|
||||
return (
|
||||
<button key={link.id} type="button" className="equipment-planner-link-chip" onClick={editing ? () => onDeleteLink(link.id) : undefined} title={editing ? textContent.deleteSocketLinkTitle || "Retirer le lien" : getSocketItemName(equipment, otherSocketItemId)}>
|
||||
<span>{getSocketItemName(equipment, otherSocketItemId)}</span>
|
||||
{editing && <Icon name="close" />}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
</article>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue