102 lines
5.5 KiB
TypeScript
102 lines
5.5 KiB
TypeScript
"use client";
|
|
|
|
import { searchItems } from "@/lib/search";
|
|
|
|
import Image from "@/components/ui/resilient-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>
|
|
);
|
|
}
|