feat(catalog): Visual Manager for Builder Club with honest canEdit

Mount BC Visual Manager, thread catalogType through tree/editor actions, and stop hardcoding canEdit=true in items/translate tabs.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
SimoandCursor committed 2026-07-18 21:03:07 +02:00
1 parent 8ccade73cc
commit 7d50b72ade
8 files changed
+457 -151

No files matched your search

@@ -5,6 +5,7 @@ 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";
@@ -13,7 +14,12 @@ 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";
import {
CatalogManagerProvider,
type CatalogManagerMode,
catalogTreeQs,
useCatalogManager,
} from "./use-catalog-store";
/**
* Visual Catalog Manager — matches habbo-next UX (full-viewport editor).
@@ -21,8 +27,12 @@ import { CatalogManagerProvider, useCatalogManager } from "./use-catalog-store";
*/
export function CatalogManagerDialog({
initialRoots = [],
catalogType = "normal",
canEdit = false,
}: {
initialRoots?: RootTab[];
catalogType?: CatalogManagerMode;
canEdit?: boolean;
}) {
const [open, setOpen] = useState(false);
const [mounted, setMounted] = useState(false);
@@ -46,6 +56,9 @@ export function CatalogManagerDialog({
};
}, [open]);
const title =
catalogType === "bc" ? "BC Visual Manager" : "Visual Manager";
return (
<>
<Button
@@ -55,7 +68,7 @@ export function CatalogManagerDialog({
onClick={() => setOpen(true)}
>
<LayoutGrid className="h-4 w-4" />
Visual Manager
{title}
</Button>
{mounted && open
? createPortal(
@@ -68,7 +81,10 @@ export function CatalogManagerDialog({
className="fixed top-[2.5vh] left-[1vw] z-[201] flex h-[95vh] max-h-[95vh] w-[98vw] max-w-[98vw] flex-col overflow-hidden rounded-lg border border-[var(--admin-border)] bg-[var(--admin-canvas)] text-[var(--admin-text)] shadow-2xl"
style={{ backgroundColor: "var(--admin-canvas)" }}
>
<CatalogManagerProvider>
<CatalogManagerProvider
catalogType={catalogType}
canEdit={canEdit}
>
<CatalogManagerInner
open={open}
onOpenChange={setOpen}
@@ -93,12 +109,15 @@ function CatalogManagerInner({
onOpenChange: (v: boolean) => void;
initialRoots: RootTab[];
}) {
const { state, dispatch } = useCatalogManager();
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");
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(
@@ -122,7 +141,7 @@ function CatalogManagerInner({
toast.error("Failed to load catalog root pages");
dispatch({ type: "SET_LOADING_ROOT", loading: false });
}
}, [dispatch, state.activeTabId]);
}, [dispatch, state.activeTabId, catQs]);
// Seed from SSR immediately, then refresh from API.
useEffect(() => {
@@ -157,13 +176,20 @@ function CatalogManagerInner({
};
const handleCreateRoot = async () => {
if (!canEdit) return;
const caption = prompt("Root page name:");
if (!caption?.trim()) return;
try {
const result = await createCatalogPage({
parentId: -1,
caption: caption.trim(),
});
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");
@@ -211,6 +237,11 @@ function CatalogManagerInner({
}
};
const heading =
catalogType === "bc"
? "BC Visual Catalog Manager"
: "Visual Catalog Manager";
return (
<TooltipProvider delayDuration={300}>
<div
@@ -222,7 +253,7 @@ function CatalogManagerInner({
className="flex items-center gap-2 text-base font-semibold leading-none"
>
<LayoutGrid className="h-4 w-4" />
Visual Catalog Manager
{heading}
</h2>
<Button
type="button"