52 lines
5.5 KiB
TypeScript
52 lines
5.5 KiB
TypeScript
"use client";
|
|
|
|
import Image from "next/image";
|
|
import Link from "next/link";
|
|
import { useRouter } from "next/navigation";
|
|
import { PlusIcon, RefreshCwIcon, RotateCcwIcon, Trash2Icon } from "lucide-react";
|
|
import { useTransition } from "react";
|
|
import { toast } from "sonner";
|
|
|
|
import { deleteStructuredGuide, moveGuideToTrash, restoreStructuredGuide, startCatalogSync } from "@/app/admin/actions";
|
|
import { DeleteConfirmationDialog } from "@/components/admin/delete-confirmation-dialog";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyTitle } from "@/components/ui/empty";
|
|
import { Spinner } from "@/components/ui/spinner";
|
|
|
|
type GuideCard = { id: string; characterKey: string; name: string; overview: string; coverUrl: string; version: number; updatedAt: Date; trashedAt: Date | null };
|
|
|
|
export function StructuredDashboard({ guides, trash, catalogReady }: { guides: GuideCard[]; trash: GuideCard[]; catalogReady: boolean }) {
|
|
const router = useRouter();
|
|
const [pending, startTransition] = useTransition();
|
|
|
|
function run(action: () => Promise<{ status: string; message?: string }>, success: string) {
|
|
startTransition(async () => {
|
|
const result = await action();
|
|
if (result.status === "error") toast.error(result.message);
|
|
else { toast.success(success); router.refresh(); }
|
|
});
|
|
}
|
|
|
|
return (
|
|
<div className="flex flex-col gap-10">
|
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
|
<div className="flex items-center gap-2"><Badge variant="outline">Lunaris {catalogReady ? "พร้อมใช้งาน" : "ยังไม่ Sync"}</Badge></div>
|
|
<div className="flex gap-2"><Button variant="outline" disabled={pending} onClick={() => run(startCatalogSync, "Sync สำเร็จแล้ว")}>{pending ? <Spinner data-icon="inline-start" /> : <RefreshCwIcon data-icon="inline-start" />}Sync Catalog</Button><Button render={<Link href="/admin/create" />} nativeButton={false} disabled={!catalogReady}><PlusIcon data-icon="inline-start" />สร้าง Guide</Button></div>
|
|
</div>
|
|
{guides.length ? (
|
|
<div className="grid gap-5 md:grid-cols-2 xl:grid-cols-3">{guides.map((guide) => (
|
|
<Card key={guide.id} className="overflow-hidden py-0">
|
|
<div className="relative aspect-square bg-muted"><Image src={guide.coverUrl} alt={`Guide ${guide.name}`} fill className="object-cover" /></div>
|
|
<CardHeader className="pt-5"><CardTitle>{guide.name}</CardTitle><CardDescription className="line-clamp-2">{guide.overview}</CardDescription><CardAction><Badge variant="secondary">v{guide.version}</Badge></CardAction></CardHeader>
|
|
<CardContent><p className="text-xs text-muted-foreground">แก้ไขล่าสุด {new Date(guide.updatedAt).toLocaleString("th-TH")}</p></CardContent>
|
|
<CardFooter className="justify-between pb-5"><Button render={<Link href={`/admin/${guide.characterKey}`} />} nativeButton={false}>แก้ไข Guide</Button><DeleteConfirmationDialog trigger={<Button variant="ghost" size="icon" />} title={`ย้าย ${guide.name} ไปถังขยะ?`} description="Guide จะหายจากหน้าเว็บไซต์ทันที และสามารถกู้คืนได้ภายหลัง" confirmLabel="ย้ายไปถังขยะ" onConfirm={() => run(() => moveGuideToTrash(guide.id), "ย้ายไปถังขยะแล้ว")}><Trash2Icon /><span className="sr-only">ย้าย {guide.name} ไปถังขยะ</span></DeleteConfirmationDialog></CardFooter>
|
|
</Card>
|
|
))}</div>
|
|
) : <Empty className="border"><EmptyHeader><EmptyTitle>ยังไม่มี Guide</EmptyTitle><EmptyDescription>Sync Catalog แล้วเลือกตัวละครเพื่อสร้าง Guide แรก</EmptyDescription></EmptyHeader><EmptyContent><Button render={<Link href="/admin/create" />} nativeButton={false} disabled={!catalogReady}>สร้าง Guide</Button></EmptyContent></Empty>}
|
|
{trash.length ? <section className="flex flex-col gap-4"><div><h2 className="font-heading text-xl font-semibold">ถังขยะ</h2><p className="text-sm text-muted-foreground">Guide ที่ซ่อนไว้จากเว็บไซต์</p></div><div className="grid gap-3 md:grid-cols-2">{trash.map((guide) => <Card key={guide.id}><CardHeader><CardTitle>{guide.name}</CardTitle><CardDescription>{guide.overview}</CardDescription><CardAction><div className="flex gap-2"><Button variant="outline" size="sm" disabled={pending} onClick={() => run(() => restoreStructuredGuide(guide.id), "กู้คืน Guide แล้ว")}><RotateCcwIcon data-icon="inline-start" />กู้คืน</Button><DeleteConfirmationDialog trigger={<Button variant="destructive" size="sm" disabled={pending} />} title={`ลบ ${guide.name} ถาวร?`} description="ข้อมูล Weapons, Artifacts, Constellations, Team และ Extra ทั้งหมดจะกู้คืนไม่ได้" confirmLabel="ยืนยันลบถาวร" onConfirm={() => run(() => deleteStructuredGuide(guide.id), "ลบ Guide ถาวรแล้ว")}><Trash2Icon data-icon="inline-start" />ลบถาวร</DeleteConfirmationDialog></div></CardAction></CardHeader></Card>)}</div></section> : null}
|
|
</div>
|
|
);
|
|
}
|