diff --git a/src/app/admin/catalog/page.tsx b/src/app/admin/catalog/page.tsx
index ca633b61..572e2407 100644
--- a/src/app/admin/catalog/page.tsx
+++ b/src/app/admin/catalog/page.tsx
@@ -9,12 +9,14 @@ import { CatalogActions } from "./catalog-actions";
import { CatalogTable } from "./catalog-table";
import { CatalogViewToggle } from "./catalog-view-toggle";
-const CatalogManagerDialog = dynamic(() =>
- import("@/components/admin/catalog-manager/catalog-manager-dialog").then(
- (m) => ({
- default: m.CatalogManagerDialog,
- }),
- ),
+const CatalogManagerDialog = dynamic(
+ () =>
+ import("@/components/admin/catalog-manager/catalog-manager-dialog").then(
+ (m) => ({
+ default: m.CatalogManagerDialog,
+ }),
+ ),
+ { ssr: false },
);
export default async function CatalogPagesPage({
params: paramsPromise,
diff --git a/src/components/admin/catalog-manager/catalog-manager-dialog.tsx b/src/components/admin/catalog-manager/catalog-manager-dialog.tsx
index a30724eb..9cc2207a 100644
--- a/src/components/admin/catalog-manager/catalog-manager-dialog.tsx
+++ b/src/components/admin/catalog-manager/catalog-manager-dialog.tsx
@@ -1,19 +1,11 @@
"use client";
-import { LayoutGrid, Plus, X } from "lucide-react";
-import { useCallback, useEffect, useRef, useState } from "react";
+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 { useConfirmDialog } from "@/components/admin/confirm-dialog";
import { Button } from "@/components/ui/button";
-import {
- Dialog,
- DialogContent,
- DialogHeader,
- DialogTitle,
-} from "@/components/ui/dialog";
-import { Input } from "@/components/ui/input";
-import { Label } from "@/components/ui/label";
import { TooltipProvider } from "@/components/ui/tooltip";
import type { RootTab } from "@/types/catalog";
import { BreadcrumbBar } from "./breadcrumb-bar";
@@ -24,30 +16,35 @@ import { StatsBar } from "./stats-bar";
import { CatalogManagerProvider, useCatalogManager } from "./use-catalog-store";
/**
- * Visual Catalog Manager — full-viewport editor (tree + inline page editor).
- * Trigger stays outside Dialog.Root (Base UI); nested dialogs are avoided.
+ * 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 dirtyRef = useRef(false);
- const { confirm, dialog: closeConfirmDialog } = useConfirmDialog();
+ const [mounted, setMounted] = useState(false);
- const handleOpenChange = async (next: boolean) => {
- if (!next && dirtyRef.current) {
- const ok = await confirm({
- title: "Unsaved changes",
- description: "You have unsaved changes. Discard them?",
- confirmLabel: "Discard",
- cancelLabel: "Keep editing",
- });
- if (!ok) return;
- }
- setOpen(next);
- };
+ 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 (
<>
- {closeConfirmDialog}
Visual Manager
-
+ {mounted && open
+ ? createPortal(
+
,
+ document.body,
+ )
+ : null}
>
);
}
@@ -84,21 +82,11 @@ export function CatalogManagerDialog() {
function CatalogManagerInner({
open,
onOpenChange,
- dirtyRef,
}: {
open: boolean;
- onOpenChange: (v: boolean) => void | Promise;
- dirtyRef: React.MutableRefObject;
+ onOpenChange: (v: boolean) => void;
}) {
const { state, dispatch } = useCatalogManager();
- const { confirm, dialog: confirmDialog } = useConfirmDialog();
- const [createOpen, setCreateOpen] = useState(false);
- const [newRootName, setNewRootName] = useState("");
- const [creating, setCreating] = useState(false);
-
- useEffect(() => {
- dirtyRef.current = state.editorDirty;
- }, [state.editorDirty, dirtyRef]);
const loadRootPages = useCallback(async () => {
dispatch({ type: "SET_LOADING_ROOT", loading: true });
@@ -138,54 +126,52 @@ function CatalogManagerInner({
if (!open) dispatch({ type: "ON_DIALOG_CLOSE" });
}, [open, dispatch]);
- async function confirmDiscardIfDirty(): Promise {
- if (!state.editorDirty) return true;
- return confirm({
- title: "Unsaved changes",
- description: "You have unsaved changes. Discard them?",
- confirmLabel: "Discard",
- cancelLabel: "Keep editing",
- });
- }
-
- const handleTabClick = async (id: number) => {
- if (!(await confirmDiscardIfDirty())) return;
+ 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 = newRootName.trim();
- if (!caption) return;
- setCreating(true);
+ const caption = prompt("Root page name:");
+ if (!caption?.trim()) return;
try {
const result = await createCatalogPage({
parentId: -1,
- caption,
+ caption: caption.trim(),
});
if (!result.ok) throw new Error(result.error);
- setCreateOpen(false);
- setNewRootName("");
await loadRootPages();
toast.success("Root page created");
} catch {
toast.error("Failed to create root page");
- } finally {
- setCreating(false);
}
};
- const handleSelectPage = async (id: number) => {
- if (!(await confirmDiscardIfDirty())) return;
+ 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 = async (id: number) => {
+ const handleEditTab = (id: number) => {
if (state.activeTabId !== id) {
- if (!(await confirmDiscardIfDirty())) return;
dispatch({ type: "SET_ACTIVE_TAB", id });
dispatch({ type: "CLEAR_TREE" });
- } else if (!(await confirmDiscardIfDirty())) {
+ }
+ if (
+ state.editorDirty &&
+ !confirm("You have unsaved changes. Discard?")
+ ) {
return;
}
dispatch({ type: "SELECT_PAGE", id });
@@ -209,25 +195,25 @@ function CatalogManagerInner({
return (
- {confirmDialog}
-
-
-
-
- Visual Catalog Manager
-
-
-
-
+
+
+
+ Visual Catalog Manager
+
+
+
@@ -236,57 +222,10 @@ function CatalogManagerInner({
activeTabId={state.activeTabId}
onTabClick={handleTabClick}
onEditTab={handleEditTab}
- onCreateRoot={() => setCreateOpen((v) => !v)}
+ onCreateRoot={handleCreateRoot}
loading={state.loadingRoot}
/>
- {createOpen ? (
-
-
-
- setNewRootName(e.target.value)}
- placeholder="e.g. Furniture"
- autoFocus
- className="h-8"
- onKeyDown={(e) => {
- if (e.key === "Enter") void handleCreateRoot();
- if (e.key === "Escape") {
- setCreateOpen(false);
- setNewRootName("");
- }
- }}
- />
-
-
-
-
- ) : null}
-
-
-
-
-
-
- New root page
-
-
+
);
}