222 lines
8.0 KiB
TypeScript
222 lines
8.0 KiB
TypeScript
"use client";
|
|
|
|
import { PlusIcon, RotateCcwIcon } from "lucide-react";
|
|
import { useMemo } from "react";
|
|
import * as z from "zod";
|
|
|
|
import {
|
|
CharacterSelect,
|
|
type AdminCatalogCharacter,
|
|
} from "@/components/admin/ui/catalog-select";
|
|
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 { ClearableInput as Input } from "@/components/admin/ui/clearable-input";
|
|
import { Switch } from "@/components/ui/switch";
|
|
import { Textarea } from "@/components/ui/textarea";
|
|
import { isValidPercent } from "@/lib/guides/percent-input";
|
|
|
|
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;
|
|
};
|
|
|
|
export function ConstellationImageGenerator({
|
|
characters,
|
|
}: {
|
|
characters: AdminCatalogCharacter[];
|
|
}) {
|
|
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) => isValidPercent(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>
|
|
<CharacterSelect
|
|
items={characters}
|
|
value={draft.characterKey}
|
|
invalid={!character}
|
|
placeholder="ค้นหาตัวละคร…"
|
|
onValueChange={(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={!isValidPercent(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={!isValidPercent(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>
|
|
);
|
|
}
|