Files
buzz-sheet/components/admin/create-guide-flow.tsx
T
2026-10-02 22:12:43 +07:00

102 lines
5.5 KiB
TypeScript

"use client";
import { searchItems } from "@/lib/search";
import Image from "next/image";
import { useRouter } from "next/navigation";
import { ArrowLeftIcon, CheckIcon, SearchIcon } from "lucide-react";
import { useMemo, useState, useTransition } from "react";
import { toast } from "sonner";
import { createStructuredGuide } from "@/app/admin/actions";
import { MediaUploader, type UploadedMedia } from "@/components/admin/media-uploader";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { Field, FieldDescription, FieldGroup, FieldLabel } from "@/components/ui/field";
import { InputGroup, InputGroupAddon, InputGroupInput } from "@/components/ui/input-group";
import { Spinner } from "@/components/ui/spinner";
import { Textarea } from "@/components/ui/textarea";
import { characterRarityGradientClass } from "@/lib/catalog/rarity";
import { cn } from "@/lib/utils";
type Character = { key: string; name: string; aliases: string[]; element: string | null; rarity: number | null; imageUrl: string };
export function CreateGuideFlow({ characters, existingKeys }: { characters: Character[]; existingKeys: string[] }) {
const router = useRouter();
const [query, setQuery] = useState("");
const [selected, setSelected] = useState<Character | null>(null);
const [cover, setCover] = useState<UploadedMedia | null>(null);
const [overview, setOverview] = useState("");
const [pending, startTransition] = useTransition();
const existing = useMemo(() => new Set(existingKeys), [existingKeys]);
const search = searchItems(characters, query,
(character) => [character.name, ...character.aliases].join(" "),
(character) => [character.name, ...character.aliases]);
const filtered = search.items;
if (selected) {
return (
<Card className="mx-auto w-full max-w-4xl">
<CardHeader>
<CardTitle>ตั้งค่า Guide ของ {selected.name}</CardTitle>
<CardDescription>เพิ่มภาพหน้าปก และใส่ Overview ได้หากต้องการ</CardDescription>
</CardHeader>
<CardContent>
<FieldGroup>
<Field>
<FieldLabel>Guide Image</FieldLabel>
{cover ? (
<div className="relative aspect-square max-w-md overflow-hidden rounded-lg border">
<Image src={cover.url} alt={`Guide cover ${selected.name}`} fill className="object-cover" />
</div>
) : null}
<MediaUploader onReady={setCover} />
</Field>
<Field>
<FieldLabel htmlFor="overview">Overview (optional)</FieldLabel>
<Textarea id="overview" value={overview} maxLength={500} onChange={(event) => setOverview(event.target.value)} placeholder="สรุปแนวทางเล่น จุดเด่น และสิ่งที่ผู้ชมจะพบใน Guide นี้" />
<FieldDescription>{overview.length}/500 ตัวอักษร</FieldDescription>
</Field>
</FieldGroup>
</CardContent>
<CardFooter className="justify-between">
<Button type="button" variant="outline" onClick={() => setSelected(null)}><ArrowLeftIcon data-icon="inline-start" />เลือกใหม่</Button>
<Button disabled={pending || !cover} onClick={() => startTransition(async () => {
const result = await createStructuredGuide({ characterKey: selected.key, coverMediaId: cover?.id, overview });
if (result.status === "error") { toast.error(result.message); return; }
toast.success("สร้าง Guide แล้ว");
router.push(result.location!);
})}>{pending ? <Spinner data-icon="inline-start" /> : <CheckIcon data-icon="inline-start" />}สร้าง Guide</Button>
</CardFooter>
</Card>
);
}
return (
<div className="flex flex-col gap-6">
<InputGroup className="max-w-xl">
<InputGroupAddon><SearchIcon /></InputGroupAddon>
<InputGroupInput value={query} onChange={(event) => setQuery(event.target.value)} placeholder="ค้นหาตัวละคร…" aria-label="ค้นหาตัวละคร" />
</InputGroup>
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
{filtered.map((character) => {
const disabled = existing.has(character.key);
return (
<Card key={character.key} className="overflow-hidden py-0">
<div className={cn("relative aspect-square", characterRarityGradientClass(character.key, character.rarity))}><Image src={character.imageUrl} alt={character.name} fill className="object-cover" /></div>
<CardHeader className="pt-4">
<CardTitle>{character.name}</CardTitle>
<CardDescription>{character.element ?? "Unknown element"}</CardDescription>
</CardHeader>
<CardContent className="flex gap-2">{character.rarity ? <Badge variant="secondary">{character.rarity}★</Badge> : null}{disabled ? <Badge variant="outline">มี Guide แล้ว</Badge> : null}</CardContent>
<CardFooter className="pb-4"><Button className="w-full" disabled={disabled} onClick={() => setSelected(character)}>เลือกตัวละคร</Button></CardFooter>
</Card>
);
})}
</div>
</div>
);
}