"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 { 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 { StatsBar } from "./stats-bar"; import { CatalogManagerProvider, useCatalogManager } from "./use-catalog-store"; /** * Visual Catalog Manager — matches habbo-next UX (full-viewport editor). * Uses a portal overlay instead of Base UI Dialog (DialogTitle/Popup break * the Radix-era layout that habbo-next still uses). */ export function CatalogManagerDialog() { 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]); return ( <> {mounted && open ? createPortal(
, document.body, ) : null} ); } function CatalogManagerInner({ open, onOpenChange, }: { open: boolean; onOpenChange: (v: boolean) => void; }) { const { state, dispatch } = useCatalogManager(); const loadRootPages = useCallback(async () => { dispatch({ type: "SET_LOADING_ROOT", loading: true }); try { const res = await fetch("/api/admin/catalog/tree?parentId=-1"); if (!res.ok) throw new Error(); const data = await res.json(); const pages: RootTab[] = (data.pages || []).map( (p: Record) => ({ id: p.id as number, caption: p.caption as string, iconImage: p.iconImage as number, enabled: String(p.enabled), orderNum: (p.orderNum as number) || 0, }), ); dispatch({ type: "SET_ROOT_PAGES", pages }); if ( pages.length > 0 && (!state.activeTabId || !pages.find((p) => p.id === state.activeTabId)) ) { 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]); // biome-ignore lint/correctness/useExhaustiveDependencies: load once when dialog opens useEffect(() => { if (open) loadRootPages(); }, [open]); 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 }); dispatch({ type: "CLEAR_TREE" }); }; const handleCreateRoot = async () => { const caption = prompt("Root page name:"); if (!caption?.trim()) return; try { const result = 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 }); dispatch({ type: "CLEAR_TREE" }); } 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 }); } }; return (

Visual Catalog Manager

); }