Files
buzz-sheet/components/admin/constellation-image-generator.tsx
T
gunshiz db554228bc
CI / Verify (push) Successful in 1m29s
CI / Build immutable images and deploy (push) Failing after 1m42s
feat(admin-images) : add standalone export generators
2026-09-29 17:13:24 +07:00

226 lines
8.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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>
);
}