"use client"; import { LayoutGrid, X } from "lucide-react"; import { useCallback, useEffect, useState } from "react"; import { createPortal } from "react-dom"; import { toast } from "sonner"; import { createCatalogPage } from "@/actions/catalog"; import { createBcPage } from "@/actions/catalog-bc"; import { Button } from "@/components/ui/button"; import { TooltipProvider } from "@/components/ui/tooltip"; import type { RootTab } from "@/types/catalog"; import { BreadcrumbBar } from "./breadcrumb-bar"; import { InlineEditor, type PageUpdateData } from "./inline-editor"; import { RootTabs } from "./root-tabs"; import { SortableTree } from "./sortable-tree"; import { type CatalogStatsTotals, StatsBar } from "./stats-bar"; import { type CatalogManagerMode, CatalogManagerProvider, catalogTreeQs, useCatalogManager, } from "./use-catalog-store"; /** * Visual Catalog Manager — matches habbo-next UX (full-viewport editor). * Portal overlay (not Base UI Dialog) so the tree + editor mount reliably. */ export function CatalogManagerDialog({ initialRoots = [], catalogType = "normal", canEdit = false, totals = null, }: { initialRoots?: RootTab[]; catalogType?: CatalogManagerMode; canEdit?: boolean; totals?: CatalogStatsTotals | null; }) { const [open, setOpen] = useState(false); const [mounted, setMounted] = useState(false); useEffect(() => setMounted(true), []); useEffect(() => { if (!open) return; const prev = document.body.style.overflow; document.body.style.overflow = "hidden"; const onKey = (e: KeyboardEvent) => { if (e.key !== "Escape") return; const tag = (e.target as HTMLElement)?.tagName; if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return; setOpen(false); }; window.addEventListener("keydown", onKey); return () => { document.body.style.overflow = prev; window.removeEventListener("keydown", onKey); }; }, [open]); const title = catalogType === "bc" ? "BC Visual Manager" : "Visual Manager"; return ( <> {mounted && open ? createPortal(
, document.body, ) : null} ); } function CatalogManagerInner({ open, onOpenChange, initialRoots, totals, }: { open: boolean; onOpenChange: (v: boolean) => void; initialRoots: RootTab[]; totals?: CatalogStatsTotals | null; }) { const { state, dispatch, catalogType, canEdit } = useCatalogManager(); const catQs = catalogTreeQs(catalogType); const loadRootPages = useCallback(async () => { dispatch({ type: "SET_LOADING_ROOT", loading: true }); try { const res = await fetch(`/api/admin/catalog/tree?parentId=-1${catQs}`); const data = await res.json().catch(() => ({})); if (!res.ok) throw new Error(data.error || "Failed to load roots"); const pages: RootTab[] = (data.pages || []).map( (p: Record) => ({ id: Number(p.id), caption: String(p.caption ?? ""), iconImage: Number(p.iconImage) || 0, enabled: String(p.enabled ?? "0"), orderNum: Number(p.orderNum) || 0, }), ); dispatch({ type: "SET_ROOT_PAGES", pages }); const activeStillValid = state.activeTabId !== null && pages.some((p) => p.id === state.activeTabId); if (pages.length > 0 && !activeStillValid) { dispatch({ type: "SET_ACTIVE_TAB", id: pages[0].id }); } } catch { toast.error("Failed to load catalog root pages"); dispatch({ type: "SET_LOADING_ROOT", loading: false }); } }, [dispatch, state.activeTabId, catQs]); // Seed from SSR immediately, then refresh from API. useEffect(() => { if (!open) return; if (initialRoots.length > 0 && state.rootPages.length === 0) { const pages = initialRoots.map((p) => ({ ...p, id: Number(p.id), enabled: String(p.enabled), })); dispatch({ type: "SET_ROOT_PAGES", pages }); if (state.activeTabId === null && pages[0]) { dispatch({ type: "SET_ACTIVE_TAB", id: pages[0].id }); } } void loadRootPages(); }, [ open, initialRoots, state.rootPages.length, state.activeTabId, dispatch, loadRootPages, ]); useEffect(() => { if (!open) dispatch({ type: "ON_DIALOG_CLOSE" }); }, [open, dispatch]); const handleTabClick = (id: number) => { if (state.editorDirty && !confirm("You have unsaved changes. Discard?")) { return; } dispatch({ type: "SET_ACTIVE_TAB", id }); }; const handleCreateRoot = async () => { if (!canEdit) return; const caption = prompt("Root page name:"); if (!caption?.trim()) return; try { const result = catalogType === "bc" ? await createBcPage({ parentId: -1, caption: caption.trim(), }) : await createCatalogPage({ parentId: -1, caption: caption.trim(), }); if (!result.ok) throw new Error(result.error); await loadRootPages(); toast.success("Root page created"); } catch { toast.error("Failed to create root page"); } }; const handleSelectPage = (id: number) => { if (state.editorDirty && !confirm("You have unsaved changes. Discard?")) { return; } dispatch({ type: "SELECT_PAGE", id: id > 0 ? id : null }); }; const handleEditTab = (id: number) => { if (state.activeTabId !== id) { dispatch({ type: "SET_ACTIVE_TAB", id }); } if (state.editorDirty && !confirm("You have unsaved changes. Discard?")) { return; } dispatch({ type: "SELECT_PAGE", id }); }; const handleEditorSaved = (updated: PageUpdateData) => { dispatch({ type: "UPDATE_NODE", update: { ...updated } }); dispatch({ type: "SET_DIRTY", dirty: false }); const rootIdx = state.rootPages.findIndex((p) => p.id === updated.id); if (rootIdx !== -1) { const pages = [...state.rootPages]; pages[rootIdx] = { ...pages[rootIdx], caption: updated.caption, enabled: updated.enabled, }; dispatch({ type: "SET_ROOT_PAGES", pages }); } }; const heading = catalogType === "bc" ? "BC Visual Catalog Manager" : "Visual Catalog Manager"; return (

{heading}

); }