Files
buzz-sheet/components/admin/constellation-image-generator.tsx
T
gunshiz a3803118ef
CI / Verify (push) Successful in 2m10s
CI / Build immutable images and deploy (push) Successful in 2m33s
feat : add analytics page
2026-10-10 20:19:49 +07:00

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>
);
}