feat : update
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import {
|
||||
ArrowDownIcon,
|
||||
ArrowUpIcon,
|
||||
ArrowUpRightIcon,
|
||||
PlusIcon,
|
||||
} from "lucide-react";
|
||||
import { useState, type ComponentProps } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import {
|
||||
createGuideExtra,
|
||||
reorderGuideExtras,
|
||||
} from "@/app/admin/actions";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Field, FieldGroup, FieldLabel } from "@/components/ui/field";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
|
||||
type ExtraLink = { id: string; slug: string; title: string };
|
||||
|
||||
export function AddExtraButton({
|
||||
guideId,
|
||||
characterKey,
|
||||
variant = "ghost",
|
||||
size = "sm",
|
||||
label = "เพิ่ม Extra",
|
||||
}: {
|
||||
guideId: string;
|
||||
characterKey: string;
|
||||
variant?: ComponentProps<typeof Button>["variant"];
|
||||
size?: ComponentProps<typeof Button>["size"];
|
||||
label?: string;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [title, setTitle] = useState("");
|
||||
const [pending, setPending] = useState(false);
|
||||
|
||||
const create = async () => {
|
||||
const trimmedTitle = title.trim();
|
||||
if (!trimmedTitle) return;
|
||||
const browserWindow = window as Window & { __guideHasUnsavedChanges?: boolean };
|
||||
if (
|
||||
browserWindow.__guideHasUnsavedChanges &&
|
||||
!window.confirm("ข้อมูลยังบันทึกไม่เสร็จ ต้องการสร้าง Extra ใหม่หรือไม่?")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
setPending(true);
|
||||
const result = await createGuideExtra(guideId, { title: trimmedTitle });
|
||||
setPending(false);
|
||||
if (result.status === "error") {
|
||||
toast.error(result.message);
|
||||
return;
|
||||
}
|
||||
setOpen(false);
|
||||
setTitle("");
|
||||
toast.success("สร้าง Extra แล้ว");
|
||||
router.push(`/admin/${characterKey}/${result.section.slug}`);
|
||||
router.refresh();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
type="button"
|
||||
variant={variant}
|
||||
size={size}
|
||||
aria-label={label || "เพิ่ม Extra"}
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
<PlusIcon data-icon="inline-start" />
|
||||
{label}
|
||||
</Button>
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>เพิ่ม Extra section</DialogTitle>
|
||||
<DialogDescription>
|
||||
ตั้งชื่อส่วนเสริม ระบบจะสร้างและเปิดหน้าแก้ไขให้ทันที
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<form
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
void create();
|
||||
}}
|
||||
>
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="new-extra-title">ชื่อ Extra</FieldLabel>
|
||||
<Input
|
||||
id="new-extra-title"
|
||||
value={title}
|
||||
maxLength={160}
|
||||
autoFocus
|
||||
onChange={(event) => setTitle(event.target.value)}
|
||||
/>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
<DialogFooter className="mt-4">
|
||||
<Button type="submit" disabled={!title.trim() || pending}>
|
||||
{pending ? <Spinner data-icon="inline-start" /> : <PlusIcon data-icon="inline-start" />}
|
||||
สร้าง Extra
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function ExtraOverview({
|
||||
guideId,
|
||||
characterKey,
|
||||
initialVersion,
|
||||
initialExtras,
|
||||
}: {
|
||||
guideId: string;
|
||||
characterKey: string;
|
||||
initialVersion: number;
|
||||
initialExtras: ExtraLink[];
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const [extras, setExtras] = useState(initialExtras);
|
||||
const [version, setVersion] = useState(initialVersion);
|
||||
const [pendingId, setPendingId] = useState<string | null>(null);
|
||||
|
||||
const move = async (index: number, direction: -1 | 1) => {
|
||||
const target = index + direction;
|
||||
if (target < 0 || target >= extras.length) return;
|
||||
const previous = extras;
|
||||
const next = [...extras];
|
||||
[next[index], next[target]] = [next[target], next[index]];
|
||||
setExtras(next);
|
||||
setPendingId(extras[index].id);
|
||||
const result = await reorderGuideExtras(guideId, {
|
||||
expectedVersion: version,
|
||||
sectionIds: next.map((extra) => extra.id),
|
||||
});
|
||||
setPendingId(null);
|
||||
if (result.status !== "saved") {
|
||||
setExtras(previous);
|
||||
toast.error(result.message);
|
||||
if (result.status === "conflict") window.location.reload();
|
||||
return;
|
||||
}
|
||||
if (result.version) setVersion(result.version);
|
||||
router.refresh();
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="flex flex-col gap-4">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<h2 className="font-heading text-xl font-semibold">Extra sections</h2>
|
||||
<p className="text-sm text-muted-foreground">แต่ละ Extra มีหน้าแก้ไขและบันทึกแยกจากกัน</p>
|
||||
</div>
|
||||
<AddExtraButton
|
||||
guideId={guideId}
|
||||
characterKey={characterKey}
|
||||
variant="outline"
|
||||
/>
|
||||
</div>
|
||||
{extras.length ? (
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
{extras.map((extra, index) => (
|
||||
<Card key={extra.id}>
|
||||
<CardHeader>
|
||||
<CardTitle>{extra.title}</CardTitle>
|
||||
<CardDescription>Extra section {index + 1}</CardDescription>
|
||||
<CardAction className="flex items-center gap-1">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
disabled={index === 0 || pendingId !== null}
|
||||
aria-label={`เลื่อน ${extra.title} ขึ้น`}
|
||||
onClick={() => void move(index, -1)}
|
||||
>
|
||||
<ArrowUpIcon />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
disabled={index === extras.length - 1 || pendingId !== null}
|
||||
aria-label={`เลื่อน ${extra.title} ลง`}
|
||||
onClick={() => void move(index, 1)}
|
||||
>
|
||||
<ArrowDownIcon />
|
||||
</Button>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Button
|
||||
variant="outline"
|
||||
render={<Link href={`/admin/${characterKey}/${extra.slug}`} />}
|
||||
nativeButton={false}
|
||||
>
|
||||
แก้ไข Extra
|
||||
<ArrowUpRightIcon data-icon="inline-end" />
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>ยังไม่มี Extra</CardTitle>
|
||||
<CardDescription>กดเพิ่ม Extra เพื่อสร้างส่วนเสริมแรกของ Guide นี้</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user