feat(extra) : add catalog items and image export
CI / Verify (push) Successful in 1m1s
CI / Build immutable images and deploy (push) Successful in 2m19s

This commit is contained in:
2026-09-05 12:36:00 +07:00 Unverified
parent 5af38194ba
commit a4ecdf4fff
11 changed files with 3685 additions and 10 deletions
+112 -1
View File
@@ -4,6 +4,8 @@ import dynamic from "next/dynamic";
import Image from "next/image";
import {
ChartNoAxesCombinedIcon,
ChevronDownIcon,
ChevronUpIcon,
ClipboardCopyIcon,
ClipboardPasteIcon,
CopyIcon,
@@ -175,6 +177,10 @@ type CatalogItem = {
element?: string | null;
elementIconUrl?: string | null;
};
type ExtraCatalogItem = {
kind: "character" | "weapon" | "artifact";
key: string;
};
const teamMemberApplyOptions = [
["characterKey", "Character"],
["weaponKey", "Weapon"],
@@ -323,6 +329,7 @@ export interface GuideSectionEditorProps {
body: string;
percent: string | null;
backgroundColor: string | null;
catalogItems: ExtraCatalogItem[];
highlights: Array<{ text: string; color: string }>;
imageUrl: string | null;
imageWidth: number | null;
@@ -3304,7 +3311,85 @@ function TeamEditor({
);
}
function ExtraEditor({ guide, initial }: EditorBase) {
function ExtraCatalogItemsEditor({
value,
characters,
weapons,
artifacts,
onChange,
}: {
value: ExtraCatalogItem[];
characters: CatalogItem[];
weapons: CatalogItem[];
artifacts: CatalogItem[];
onChange: (value: ExtraCatalogItem[]) => void;
}) {
const [kind, setKind] = useState<ExtraCatalogItem["kind"]>("character");
const [key, setKey] = useState("");
const catalogs = { character: characters, weapon: weapons, artifact: artifacts };
const labels = { character: "ตัวละคร", weapon: "อาวุธ", artifact: "อาร์ติแฟกต์" };
const items = catalogs[kind];
return (
<Field>
<FieldLabel>ตัวละคร อาวุธ และอาร์ติแฟกต์</FieldLabel>
<div className="flex flex-col gap-3">
{value.length ? (
<div className="flex flex-col gap-2">
{value.map((reference, index) => {
const item = catalogs[reference.kind].find(
(candidate) => candidate.key === reference.key,
);
return (
<Card key={`${reference.kind}:${reference.key}`} size="sm">
<CardContent className="flex items-center gap-3">
{item ? (
<div className={cn("relative size-12 shrink-0 overflow-hidden rounded-md", rarityGradientClass(item.rarity))}>
<Image src={item.imageUrl} alt="" fill className="object-contain" />
</div>
) : null}
<div className="min-w-0 flex-1">
<p className="truncate font-medium">{item?.name ?? reference.key}</p>
<p className="text-sm text-muted-foreground">{labels[reference.kind]}</p>
</div>
<Button type="button" variant="ghost" size="icon-sm" disabled={index === 0} onClick={() => onChange(arrayMove(value, index, index - 1))} aria-label="เลื่อนขึ้น">
<ChevronUpIcon />
</Button>
<Button type="button" variant="ghost" size="icon-sm" disabled={index === value.length - 1} onClick={() => onChange(arrayMove(value, index, index + 1))} aria-label="เลื่อนลง">
<ChevronDownIcon />
</Button>
<Button type="button" variant="ghost" size="icon-sm" onClick={() => onChange(value.filter((_, itemIndex) => itemIndex !== index))} aria-label={`ลบ ${item?.name ?? reference.key}`}>
<XIcon />
</Button>
</CardContent>
</Card>
);
})}
</div>
) : null}
<div className="flex flex-wrap items-center gap-2">
<Select value={kind} onValueChange={(next) => { setKind(next as ExtraCatalogItem["kind"]); setKey(""); }}>
<SelectTrigger className="w-40"><SelectValue /></SelectTrigger>
<SelectContent><SelectGroup>
<SelectItem value="character">ตัวละคร</SelectItem>
<SelectItem value="weapon">อาวุธ</SelectItem>
<SelectItem value="artifact">อาร์ติแฟกต์</SelectItem>
</SelectGroup></SelectContent>
</Select>
<div className="min-w-64 flex-1">
<CatalogPicker items={items} value={key} onChange={setKey} placeholder={`เลือก${labels[kind]}`} showSelectedImage rarityBackground />
</div>
<Button type="button" variant="outline" disabled={!key || value.some((item) => item.kind === kind && item.key === key)} onClick={() => { onChange([...value, { kind, key }]); setKey(""); }}>
<PlusIcon data-icon="inline-start" />
เพิ่มรายการ
</Button>
</div>
</div>
</Field>
);
}
function ExtraEditor({ guide, initial, characters, weapons, artifacts }: EditorBase) {
const extraSensors = useSensors(
useSensor(PointerSensor),
useSensor(KeyboardSensor, {
@@ -3338,6 +3423,7 @@ function ExtraEditor({ guide, initial }: EditorBase) {
? null
: Number(row.percent),
backgroundColor: row.backgroundColor,
catalogItems: row.catalogItems,
highlights: [],
})),
})),
@@ -3369,6 +3455,7 @@ function ExtraEditor({ guide, initial }: EditorBase) {
id: row.id || createClientId("extra-row-record"),
body: normalizeExtraRichText(row.body, row.highlights ?? []),
backgroundColor: row.backgroundColor ?? null,
catalogItems: row.catalogItems ?? [],
highlights: [],
clientId: row.clientId || createClientId("extra-row"),
})),
@@ -3621,6 +3708,28 @@ function ExtraEditor({ guide, initial }: EditorBase) {
)
}
/>
<ExtraCatalogItemsEditor
value={row.catalogItems}
characters={characters}
weapons={weapons}
artifacts={artifacts}
onChange={(catalogItems) =>
setSections(
sections.map((item, index) =>
index === sectionIndex
? {
...item,
rows: item.rows.map((candidate, inner) =>
inner === rowIndex
? { ...candidate, catalogItems }
: candidate,
),
}
: item,
),
)
}
/>
<Field>
<FieldLabel>หัวข้อ</FieldLabel>
<Input
@@ -3657,6 +3766,7 @@ function ExtraEditor({ guide, initial }: EditorBase) {
<FieldLabel>ข้อความ</FieldLabel>
<RichTextEditor
value={row.body}
showBubbleMenu={false}
invalid={extraIssue(
"sections",
sectionIndex,
@@ -3837,6 +3947,7 @@ function ExtraEditor({ guide, initial }: EditorBase) {
body: "",
percent: null,
backgroundColor: null,
catalogItems: [],
highlights: [],
clientId: createClientId("extra-row"),
},