226 lines
8.1 KiB
TypeScript
226 lines
8.1 KiB
TypeScript
"use client";
|
||
|
||
import { PlusIcon, RotateCcwIcon } from "lucide-react";
|
||
import { useMemo } from "react";
|
||
import * as z from "zod";
|
||
|
||
import {
|
||
ImageCatalogPicker,
|
||
type ImageCatalogCharacter,
|
||
} from "@/components/admin/image-generator-fields";
|
||
import { useImageGeneratorDraft } from "@/components/admin/use-image-generator-draft";
|
||
import {
|
||
ConstellationImage,
|
||
type ConstellationImageGroup,
|
||
type ConstellationImageGuide,
|
||
} from "@/components/public/constellation-image";
|
||
import { GuideImageExport } from "@/components/public/guide-image-export";
|
||
import { Button } from "@/components/ui/button";
|
||
import {
|
||
Card,
|
||
CardAction,
|
||
CardContent,
|
||
CardDescription,
|
||
CardHeader,
|
||
CardTitle,
|
||
} from "@/components/ui/card";
|
||
import {
|
||
Empty,
|
||
EmptyContent,
|
||
EmptyDescription,
|
||
EmptyHeader,
|
||
EmptyMedia,
|
||
EmptyTitle,
|
||
} from "@/components/ui/empty";
|
||
import { Field, FieldGroup, FieldLabel } from "@/components/ui/field";
|
||
import { Input } from "@/components/ui/input";
|
||
import { Switch } from "@/components/ui/switch";
|
||
import { Textarea } from "@/components/ui/textarea";
|
||
|
||
const rowSchema = z.object({
|
||
level: z.number().int().min(1).max(6),
|
||
cumulativePercent: z.string(),
|
||
isGreat: z.boolean(),
|
||
});
|
||
|
||
const draftSchema = z.object({
|
||
characterKey: z.string(),
|
||
note: z.string(),
|
||
rows: z.array(rowSchema).max(6).refine((rows) => rows.length === 0 || rows.length === 6),
|
||
});
|
||
|
||
type ConstellationDraft = z.infer<typeof draftSchema>;
|
||
|
||
const INITIAL_CONSTELLATION_DRAFT: ConstellationDraft = {
|
||
characterKey: "",
|
||
note: "",
|
||
rows: [],
|
||
};
|
||
|
||
const constellationRows = (): ConstellationDraft["rows"] =>
|
||
Array.from({ length: 6 }, (_, index) => ({
|
||
level: index + 1,
|
||
cumulativePercent: "100",
|
||
isGreat: false,
|
||
}));
|
||
|
||
const parseConstellationDraft = (value: unknown): ConstellationDraft | null => {
|
||
const parsed = draftSchema.safeParse(value);
|
||
return parsed.success ? parsed.data : null;
|
||
};
|
||
|
||
function validPercent(value: string): boolean {
|
||
const number = Number(value);
|
||
return value.trim().length > 0 && Number.isFinite(number) && number >= -9999 && number <= 9999;
|
||
}
|
||
|
||
export function ConstellationImageGenerator({
|
||
characters,
|
||
}: {
|
||
characters: ImageCatalogCharacter[];
|
||
}) {
|
||
const [draft, setDraft, resetDraft] = useImageGeneratorDraft(
|
||
"buzz-sheet:admin-image:constellation",
|
||
INITIAL_CONSTELLATION_DRAFT,
|
||
parseConstellationDraft,
|
||
);
|
||
const character = characters.find((item) => item.key === draft.characterKey);
|
||
const valid = Boolean(character && draft.rows.every((row) => validPercent(row.cumulativePercent)));
|
||
|
||
const guide = useMemo<ConstellationImageGuide>(() => ({
|
||
coverUrl: character?.imageUrl ?? "",
|
||
character: {
|
||
name: character?.name ?? "Character",
|
||
splashImageUrl: character?.splashImageUrl ?? character?.imageUrl ?? null,
|
||
},
|
||
}), [character]);
|
||
|
||
const group = useMemo<ConstellationImageGroup>(() => ({
|
||
note: draft.note.trim() || null,
|
||
rows: draft.rows,
|
||
}), [draft.note, draft.rows]);
|
||
|
||
return (
|
||
<div className="grid items-start gap-6 xl:grid-cols-[minmax(24rem,1fr)_minmax(28rem,1fr)]">
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle>ข้อมูล Constellation</CardTitle>
|
||
<CardDescription>กรอกดาเมจสะสม C1–C6 เทียบกับ C0 ที่ 100%</CardDescription>
|
||
<CardAction>
|
||
<Button type="button" variant="ghost" size="sm" onClick={resetDraft}>
|
||
<RotateCcwIcon data-icon="inline-start" />
|
||
Reset
|
||
</Button>
|
||
</CardAction>
|
||
</CardHeader>
|
||
<CardContent className="flex flex-col gap-5">
|
||
{!draft.rows.length ? (
|
||
<Empty className="border">
|
||
<EmptyHeader>
|
||
<EmptyMedia variant="icon"><PlusIcon /></EmptyMedia>
|
||
<EmptyTitle>ยังไม่มี Constellation</EmptyTitle>
|
||
<EmptyDescription>สร้างรายการ C1–C6 เพื่อเริ่มกรอกเปอร์เซ็นต์ดาเมจ</EmptyDescription>
|
||
</EmptyHeader>
|
||
<EmptyContent>
|
||
<Button type="button" onClick={() => setDraft((current) => ({ ...current, rows: constellationRows() }))}>
|
||
<PlusIcon data-icon="inline-start" />
|
||
สร้าง Constellation
|
||
</Button>
|
||
</EmptyContent>
|
||
</Empty>
|
||
) : (
|
||
<>
|
||
<FieldGroup>
|
||
<Field data-invalid={!character}>
|
||
<FieldLabel>ตัวละคร</FieldLabel>
|
||
<ImageCatalogPicker
|
||
items={characters}
|
||
value={draft.characterKey}
|
||
invalid={!character}
|
||
placeholder="ค้นหาตัวละคร…"
|
||
onChange={(characterKey) => setDraft((current) => ({ ...current, characterKey }))}
|
||
/>
|
||
</Field>
|
||
<Field>
|
||
<FieldLabel htmlFor="constellation-image-note">โน้ตท้ายภาพ (ถ้ามี)</FieldLabel>
|
||
<Textarea
|
||
id="constellation-image-note"
|
||
value={draft.note}
|
||
onChange={(event) => setDraft((current) => ({ ...current, note: event.target.value }))}
|
||
/>
|
||
</Field>
|
||
</FieldGroup>
|
||
|
||
<FieldGroup>
|
||
{draft.rows.map((row, index) => (
|
||
<Field key={row.level} data-invalid={!validPercent(row.cumulativePercent)}>
|
||
<div className="grid grid-cols-[3rem_minmax(0,1fr)_auto] items-center gap-3">
|
||
<FieldLabel htmlFor={`constellation-image-c${row.level}`}>
|
||
C{row.level}
|
||
</FieldLabel>
|
||
<Input
|
||
id={`constellation-image-c${row.level}`}
|
||
type="number"
|
||
step="0.01"
|
||
value={row.cumulativePercent}
|
||
aria-invalid={!validPercent(row.cumulativePercent)}
|
||
onChange={(event) => setDraft((current) => ({
|
||
...current,
|
||
rows: current.rows.map((item, rowIndex) => rowIndex === index
|
||
? { ...item, cumulativePercent: event.target.value }
|
||
: item),
|
||
}))}
|
||
/>
|
||
<Field orientation="horizontal">
|
||
<Switch
|
||
id={`constellation-image-great-${row.level}`}
|
||
checked={row.isGreat}
|
||
onCheckedChange={(isGreat) => setDraft((current) => ({
|
||
...current,
|
||
rows: current.rows.map((item, rowIndex) => rowIndex === index
|
||
? { ...item, isGreat }
|
||
: item),
|
||
}))}
|
||
/>
|
||
<FieldLabel htmlFor={`constellation-image-great-${row.level}`}>เด่น</FieldLabel>
|
||
</Field>
|
||
</div>
|
||
</Field>
|
||
))}
|
||
</FieldGroup>
|
||
</>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle>ภาพส่งออก</CardTitle>
|
||
<CardDescription>ใช้กราฟเดียวกับ Constellation export ใน Guide</CardDescription>
|
||
</CardHeader>
|
||
<CardContent>
|
||
{!draft.rows.length ? (
|
||
<Empty className="border">
|
||
<EmptyHeader>
|
||
<EmptyTitle>ยังไม่มี Preview</EmptyTitle>
|
||
<EmptyDescription>สร้าง Constellation แล้วกรอกข้อมูลเพื่อดูภาพส่งออก</EmptyDescription>
|
||
</EmptyHeader>
|
||
</Empty>
|
||
) : (
|
||
<GuideImageExport
|
||
fileName={`${character?.name ?? "character"}-Constellations.png`}
|
||
width={1500}
|
||
height={1500}
|
||
captureClassName="p-0"
|
||
modeSelection="inline"
|
||
disabled={!valid}
|
||
>
|
||
<ConstellationImage guide={guide} group={group} />
|
||
</GuideImageExport>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
);
|
||
}
|