"use client"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { CommandIcon, EyeIcon, MonitorIcon, PanelRightIcon, Redo2Icon, SmartphoneIcon, TabletIcon, Undo2Icon, } from "lucide-react"; import { useEffect, useRef, useState } from "react"; import { toast } from "sonner"; import { saveEditorDataSource, saveEditorPage, type EditorSaveInput, } from "@/app/admin/actions"; import { AdminCommandDialog } from "@/components/admin/admin-command"; import { AdminSidebar, type AdminNavigation, } from "@/components/admin/admin-sidebar"; import { BlockCanvas } from "@/components/admin/block-canvas"; import { EditorInspector } from "@/components/admin/editor-inspector"; import type { UploadedMedia } from "@/components/admin/media-uploader"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { Badge } from "@/components/ui/badge"; import { Breadcrumb, BreadcrumbItem, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, } from "@/components/ui/breadcrumb"; import { Button } from "@/components/ui/button"; import { Drawer, DrawerContent, DrawerDescription, DrawerHeader, DrawerTitle, DrawerTrigger, } from "@/components/ui/drawer"; import { ResizableHandle, ResizablePanel, ResizablePanelGroup, } from "@/components/ui/resizable"; import { SidebarInset, SidebarProvider, SidebarTrigger, } from "@/components/ui/sidebar"; import { Spinner } from "@/components/ui/spinner"; import { Switch } from "@/components/ui/switch"; import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; import type { SerializedBlock } from "@/lib/blocks"; import type { DataSourceSnapshot, PageSnapshot } from "@/lib/content/types"; import { AutosaveCoordinator, type AutosaveStatus, } from "@/lib/editor/autosave"; import { draftFromSnapshot, normalizeBlockOrder, type EditorDraft, } from "@/lib/editor/draft"; import { cn } from "@/lib/utils"; type Viewport = "mobile" | "tablet" | "desktop"; interface HistoryState { items: EditorDraft[]; index: number; } export function EditorWorkspace({ snapshot, navigation, }: { snapshot: PageSnapshot; navigation: AdminNavigation; }) { const router = useRouter(); const initialDraft = draftFromSnapshot(snapshot); const [history, setHistory] = useState({ items: [initialDraft], index: 0 }); const draft = history.items[history.index]; const [selectedId, setSelectedId] = useState(draft.blocks[0]?.id ?? null); const [commandOpen, setCommandOpen] = useState(false); const [viewport, setViewport] = useState("desktop"); const [autosaveStatus, setAutosaveStatus] = useState({ state: "idle" }); const [dataSources, setDataSources] = useState(snapshot.dataSources); const [sourceSaving, setSourceSaving] = useState(false); const coordinatorRef = useRef | null>(null); useEffect(() => { const coordinator = new AutosaveCoordinator({ initialVersion: snapshot.page.version, createPayload: (next, expectedVersion) => ({ ...next, expectedVersion }), save: saveEditorPage, onStatus: setAutosaveStatus, onSaved: (savedSnapshot) => { if (savedSnapshot) setDataSources(savedSnapshot.dataSources); }, }); coordinatorRef.current = coordinator; return () => { coordinator.dispose(); coordinatorRef.current = null; }; }, [snapshot.page.id, snapshot.page.version]); useEffect(() => { const onKeyDown = (event: KeyboardEvent) => { if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") { event.preventDefault(); setCommandOpen(true); } }; window.addEventListener("keydown", onKeyDown); return () => window.removeEventListener("keydown", onKeyDown); }, []); function commit(next: EditorDraft) { setHistory((current) => ({ items: [...current.items.slice(0, current.index + 1), structuredClone(next)].slice(-80), index: Math.min(current.index + 1, 79), })); coordinatorRef.current?.schedule(next); } function travel(direction: -1 | 1) { setHistory((current) => { const index = current.index + direction; if (index < 0 || index >= current.items.length) return current; coordinatorRef.current?.schedule(current.items[index]); return { ...current, index }; }); } function insert(block: SerializedBlock) { const blocks = normalizeBlockOrder([...draft.blocks, block]); commit({ ...draft, blocks }); setSelectedId(block.id); } async function saveSource(source: DataSourceSnapshot) { setSourceSaving(true); const result = await saveEditorDataSource(source); setSourceSaving(false); if (result.status === "saved") { if (result.snapshot) { setDataSources((current) => ({ ...current, [source.id]: result.snapshot! })); } toast.success("บันทึกแหล่งข้อมูลแล้ว"); } else if (result.status === "conflict") { toast.error("แหล่งข้อมูลมีการแก้ไขจากที่อื่น กรุณาโหลดหน้าใหม่"); } else { toast.error("บันทึกแหล่งข้อมูลไม่สำเร็จ"); } } function addUploadedMedia(media: UploadedMedia) { const block: SerializedBlock = { id: crypto.randomUUID(), type: "image", schemaVersion: 1, config: { mediaId: media.id, alt: media.fileName.replace(/\.[^.]+$/u, ""), fit: "cover", }, responsiveLayout: { width: "wide", mobileSpan: 4, tabletSpan: 8, desktopSpan: 12, hideOnMobile: false, }, sortOrder: draft.blocks.length, }; insert(block); } const inspector = ( setDataSources((current) => ({ ...current, [source.id]: source }))} onSaveDataSource={saveSource} sourceSaving={sourceSaving} onMediaReady={addUploadedMedia} /> ); return (
{snapshot.character.nameTh} {draft.title} { const next = values.at(-1) as Viewport | undefined; if (next) setViewport(next); }} variant="outline" size="sm" spacing={0} className="hidden lg:flex" > }> Inspector และ Live Preview แก้ไขคุณสมบัติบล็อกและดูตัวอย่าง {inspector}
setSelectedId(id || null)} onChange={commit} onOpenInsert={() => setCommandOpen(true)} />
{inspector}
พบการแก้ไขที่ใหม่กว่า เวอร์ชันบนเซิร์ฟเวอร์เปลี่ยนไประหว่างที่คุณกำลังทำงาน ฉบับในเครื่องยังไม่ถูกเขียนทับ คุณสามารถกลับไปตรวจเทียบหรือโหลดฉบับล่าสุด กลับไปตรวจเทียบ router.refresh()}>โหลดเวอร์ชันล่าสุด
); } function SaveStatus({ status }: { status: AutosaveStatus }) { if (status.state === "saving") return กำลังบันทึก; if (status.state === "dirty") return รอบันทึก; if (status.state === "saved") return บันทึกแล้ว · v{status.version}; if (status.state === "conflict") return ข้อมูลชนกัน; if (status.state === "error") return บันทึกไม่สำเร็จ; return พร้อมแก้ไข; }