feat : add toast when delete
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import type { ComponentProps, ReactNode } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import {
|
||||
AlertDialog,
|
||||
@@ -23,6 +24,7 @@ interface DeleteConfirmationDialogProps {
|
||||
children?: ReactNode;
|
||||
open?: ComponentProps<typeof AlertDialog>["open"];
|
||||
onOpenChange?: ComponentProps<typeof AlertDialog>["onOpenChange"];
|
||||
showSuccessToast?: boolean;
|
||||
}
|
||||
|
||||
export function DeleteConfirmationDialog({
|
||||
@@ -34,7 +36,13 @@ export function DeleteConfirmationDialog({
|
||||
children,
|
||||
open,
|
||||
onOpenChange,
|
||||
showSuccessToast = true,
|
||||
}: DeleteConfirmationDialogProps) {
|
||||
const handleConfirm = async () => {
|
||||
await onConfirm();
|
||||
if (showSuccessToast) toast.success("ลบสำเร็จ!");
|
||||
};
|
||||
|
||||
return (
|
||||
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
||||
{trigger ? (
|
||||
@@ -47,7 +55,7 @@ export function DeleteConfirmationDialog({
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>ยกเลิก</AlertDialogCancel>
|
||||
<AlertDialogAction variant="destructive" onClick={onConfirm}>
|
||||
<AlertDialogAction variant="destructive" onClick={handleConfirm}>
|
||||
{confirmLabel}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
|
||||
@@ -3,52 +3,273 @@
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { PlusIcon, RefreshCwIcon, RotateCcwIcon, Trash2Icon } from "lucide-react";
|
||||
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 {
|
||||
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 {
|
||||
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 };
|
||||
type GuideCard = {
|
||||
id: string;
|
||||
characterKey: string;
|
||||
name: string;
|
||||
overview: string;
|
||||
coverUrl: string;
|
||||
version: number;
|
||||
updatedAt: Date;
|
||||
trashedAt: Date | null;
|
||||
};
|
||||
|
||||
export function StructuredDashboard({ guides, trash, catalogVersion, latestCatalogVersion }: { guides: GuideCard[]; trash: GuideCard[]; catalogVersion: string | null; latestCatalogVersion: string | null }) {
|
||||
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);
|
||||
const updateAvailable = Boolean(
|
||||
catalogVersion &&
|
||||
latestCatalogVersion &&
|
||||
catalogVersion !== latestCatalogVersion,
|
||||
);
|
||||
|
||||
function run(action: () => Promise<{ status: string; message?: string }>, success: string) {
|
||||
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(); }
|
||||
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 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="justify-between pb-5"><Button className="relative z-20" render={<Link href={`/admin/${guide.characterKey}`} />} nativeButton={false}>แก้ไข Guide</Button><DeleteConfirmationDialog 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 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 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="justify-between pb-5">
|
||||
<Button
|
||||
className="relative z-20"
|
||||
render={<Link href={`/admin/${guide.characterKey}`} />}
|
||||
nativeButton={false}
|
||||
>
|
||||
แก้ไข Guide
|
||||
</Button>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user