284 lines
9.7 KiB
TypeScript
284 lines
9.7 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 { GuideVisibilityControl } from "@/components/admin/guide-visibility-control";
|
|
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;
|
|
isPublic: boolean;
|
|
};
|
|
|
|
export function StructuredDashboard({
|
|
guides,
|
|
trash,
|
|
catalogVersion,
|
|
latestCatalogVersion,
|
|
}: {
|
|
guides: GuideCard[];
|
|
trash: GuideCard[];
|
|
catalogVersion: string | null;
|
|
latestCatalogVersion: string | null;
|
|
}) {
|
|
const router = useRouter();
|
|
const [pending, startTransition] = useTransition();
|
|
const catalogReady = Boolean(catalogVersion);
|
|
const updateAvailable = Boolean(
|
|
catalogVersion &&
|
|
latestCatalogVersion &&
|
|
catalogVersion !== latestCatalogVersion,
|
|
);
|
|
|
|
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 flex-wrap items-center gap-2">
|
|
<Badge variant="outline">
|
|
Lunaris ที่ Sync:{" "}
|
|
{catalogVersion ? `v${catalogVersion}` : "ยังไม่ Sync"}
|
|
</Badge>
|
|
<Badge variant={updateAvailable ? "default" : "secondary"}>
|
|
{latestCatalogVersion
|
|
? updateAvailable
|
|
? `มีอัปเดต: v${latestCatalogVersion}`
|
|
: `ล่าสุด: v${latestCatalogVersion}`
|
|
: "ตรวจสอบเวอร์ชันล่าสุดไม่ได้"}
|
|
</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="relative overflow-hidden py-0 transition-colors hover:bg-muted/50"
|
|
>
|
|
<Link
|
|
href={`/admin/${guide.characterKey}`}
|
|
prefetch
|
|
className="absolute inset-0 z-10 rounded-xl outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
>
|
|
<span className="sr-only">แก้ไข Guide {guide.name}</span>
|
|
</Link>
|
|
<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="flex-wrap justify-between gap-2 pb-5">
|
|
<Button
|
|
className="relative z-20"
|
|
render={<Link href={`/admin/${guide.characterKey}`} />}
|
|
nativeButton={false}
|
|
>
|
|
แก้ไข Guide
|
|
</Button>
|
|
<GuideVisibilityControl
|
|
className="relative z-20"
|
|
guideId={guide.id}
|
|
guideName={guide.name}
|
|
isPublic={guide.isPublic}
|
|
/>
|
|
<DeleteConfirmationDialog
|
|
showSuccessToast={false}
|
|
trigger={
|
|
<Button
|
|
className="relative z-20"
|
|
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
|
|
showSuccessToast={false}
|
|
trigger={
|
|
<Button
|
|
variant="destructive"
|
|
size="sm"
|
|
disabled={pending}
|
|
/>
|
|
}
|
|
title={`ลบ ${guide.name} ถาวร?`}
|
|
description="ข้อมูล Weapons, Artifacts, Constellations, Team และ Extra ทั้งหมดจะกู้คืนไม่ได้"
|
|
confirmLabel="ยืนยันลบถาวร"
|
|
onConfirm={() =>
|
|
run(
|
|
() => deleteStructuredGuide(guide.id),
|
|
"ลบสำเร็จ!",
|
|
)
|
|
}
|
|
>
|
|
<Trash2Icon data-icon="inline-start" />
|
|
ลบถาวร
|
|
</DeleteConfirmationDialog>
|
|
</div>
|
|
</CardAction>
|
|
</CardHeader>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
</section>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|