"use client"; import { ChevronRight, ChevronsDownUp, ChevronsUpDown, Copy, Eye, EyeOff, EyeOff as EyeOffIcon, FolderTree, Loader2, Package, Pencil, Plus, Power, PowerOff, Search, ToggleLeft, ToggleRight, Trash2, X, } from "lucide-react"; import { useRouter } from "next/navigation"; import { createContext, useCallback, useContext, useEffect, useRef, useState, } from "react"; import { toast } from "sonner"; import { useConfirmDialog } from "@/components/admin/confirm-dialog"; import Link from "@/components/link"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuLabel, ContextMenuSeparator, ContextMenuTrigger, } from "@/components/ui/context-menu"; import { Input } from "@/components/ui/input"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; import { adminFetch } from "@/lib/admin-fetch"; import { catalogueIconUrl } from "@/lib/catalog-assets"; import { translateCaption } from "@/lib/catalog-translations"; import { cn } from "@/lib/utils"; /* ─── Types ──────────────────────────────────────────────── */ interface TreeNode { id: number; caption: string; parentId: number; enabled: string; visible: string; iconImage: number; iconColor: number; pageLayout: string; childCount: number; itemCount: number; } interface CatalogTreeProps { rootPages: TreeNode[]; catalogType?: "normal" | "bc"; } /* ─── Context for expand/collapse all ────────────────────── */ interface TreeContext { expandSignal: number; collapseSignal: number; filterMode: FilterMode; onToggleEnabled: (node: TreeNode) => void; onToggleVisible: (node: TreeNode) => void; onDeletePage: (node: TreeNode) => void; searchQuery: string; catalogType: "normal" | "bc"; } const TreeCtx = createContext({ expandSignal: 0, collapseSignal: 0, filterMode: "all", onToggleEnabled: () => {}, onToggleVisible: () => {}, onDeletePage: () => {}, searchQuery: "", catalogType: "normal", }); /* ─── Catalog Icon ───────────────────────────────────────── */ export function CatalogIcon({ iconImage, size = 20, }: { iconImage: number; size?: number; }) { const [error, setError] = useState(false); if (error || iconImage <= 0) { return ; } return ( setError(true)} /> ); } /* ─── Layout label helper ────────────────────────────────── */ export const LAYOUT_COLORS: Record = { default_3x3: "bg-[var(--admin-info-subtle)] text-[var(--admin-info)] dark:text-[var(--admin-info)]", frontpage: "bg-[var(--admin-warning-subtle)] text-[var(--admin-warning)] dark:text-[var(--admin-warning)]", spaces_new: "bg-[var(--admin-success-subtle)] text-[var(--admin-success)] dark:text-[var(--admin-success)]", pets: "bg-[var(--admin-error)]/15 text-[var(--admin-error)]", pets2: "bg-[var(--admin-error)]/15 text-[var(--admin-error)]", pets3: "bg-[var(--admin-error)]/15 text-[var(--admin-error)]", marketplace: "bg-[var(--admin-accent)]/15 text-[var(--admin-accent-text)]", recycler: "bg-[var(--admin-success-subtle)] text-[var(--admin-success)] dark:text-[var(--admin-success)]", club_buy: "bg-[var(--admin-warning-subtle)] text-[var(--admin-warning)] dark:text-[var(--admin-warning)]", single_bundle: "bg-[var(--admin-info-subtle)] text-[var(--admin-info)] dark:text-[var(--admin-info)]", room_bundle: "bg-[var(--admin-info-subtle)] text-[var(--admin-info)] dark:text-[var(--admin-info)]", }; /* ─── Highlight matched text ─────────────────────────────── */ function HighlightText({ text, query }: { text: string; query: string }) { if (!query.trim()) return <>{text}; const idx = text.toLowerCase().indexOf(query.toLowerCase()); if (idx === -1) return <>{text}; return ( <> {text.slice(0, idx)} {text.slice(idx, idx + query.length)} {text.slice(idx + query.length)} ); } /* ─── Main Tree Component ────────────────────────────────── */ type FilterMode = "all" | "enabled" | "disabled"; export function CatalogTree({ rootPages, catalogType = "normal", }: CatalogTreeProps) { const router = useRouter(); const { confirm, dialog: confirmDialog } = useConfirmDialog(); const [searchQuery, setSearchQuery] = useState(""); const [searchResults, setSearchResults] = useState(null); const [searching, setSearching] = useState(false); const searchTimeout = useRef | null>(null); const searchInputRef = useRef(null); const [expandSignal, setExpandSignal] = useState(0); const [collapseSignal, setCollapseSignal] = useState(0); const [filterMode, setFilterMode] = useState("all"); // "/" keyboard shortcut to focus search useEffect(() => { function onKeyDown(e: KeyboardEvent) { if (e.key === "/" && !e.metaKey && !e.ctrlKey && !e.altKey) { const tag = (e.target as HTMLElement)?.tagName; if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return; e.preventDefault(); searchInputRef.current?.focus(); } } document.addEventListener("keydown", onKeyDown); return () => document.removeEventListener("keydown", onKeyDown); }, []); // Debounced search useEffect(() => { if (!searchQuery.trim()) { setSearchResults(null); return; } if (searchTimeout.current) clearTimeout(searchTimeout.current); searchTimeout.current = setTimeout(async () => { setSearching(true); try { const catParam = catalogType === "bc" ? "&catalog=bc" : ""; const res = await adminFetch( `/api/admin/catalog/tree?search=${encodeURIComponent(searchQuery.trim())}${catParam}`, ); if (res.ok) { const data = await res.json(); setSearchResults(data.pages || []); } } catch (error) { console.error("[CatalogTree] Search failed:", error); } finally { setSearching(false); } }, 300); return () => { if (searchTimeout.current) clearTimeout(searchTimeout.current); }; }, [searchQuery, catalogType]); // Quick actions from context menu const catParam = catalogType === "bc" ? "&catalog=bc" : ""; const catBody = catalogType === "bc" ? ', "catalog": "bc"' : ""; const handleToggleEnabled = useCallback( async (node: TreeNode) => { try { const res = await adminFetch("/api/admin/catalog/tree", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: `{"pageId":${node.id},"toggleField":"toggleEnabled"${catBody}}`, }); if (!res.ok) throw new Error(); toast.success( `${node.caption} ${node.enabled === "1" ? "disabled" : "enabled"}`, ); router.refresh(); } catch { toast.error("Failed to toggle. Open the page to edit."); } }, [router, catBody], ); const handleToggleVisible = useCallback( async (node: TreeNode) => { try { const res = await adminFetch("/api/admin/catalog/tree", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: `{"pageId":${node.id},"toggleField":"toggleVisible"${catBody}}`, }); if (!res.ok) throw new Error(); toast.success( `${node.caption} ${node.visible === "1" ? "hidden" : "visible"}`, ); router.refresh(); } catch { toast.error("Failed to toggle. Open the page to edit."); } }, [router, catBody], ); const handleDeletePage = useCallback( async (node: TreeNode) => { const childInfo = node.childCount > 0 ? ` Its ${node.childCount} child page(s) will be moved to parent.` : ""; const itemInfo = node.itemCount > 0 ? ` ${node.itemCount} item(s) will be deleted.` : ""; const ok = await confirm({ title: "Delete page", description: `Delete "${node.caption}"?${childInfo}${itemInfo}`, confirmLabel: "Delete", }); if (!ok) return; try { const res = await adminFetch( `/api/admin/catalog/tree?pageId=${node.id}&mode=reparent${catParam}`, { method: "DELETE", }, ); if (!res.ok) throw new Error(); toast.success(`"${node.caption}" deleted`); router.refresh(); } catch { toast.error("Failed to delete page."); } }, [router, catParam, confirm], ); const unfilteredPages = searchResults ?? rootPages; const displayPages = filterMode === "all" ? unfilteredPages : unfilteredPages.filter((p) => filterMode === "enabled" ? p.enabled === "1" : p.enabled !== "1", ); return ( {confirmDialog}
{/* Header */}

Catalog Structure

{displayPages.length} {filterMode !== "all" ? ` / ${unfilteredPages.length}` : ""}{" "} pages
{/* Status filter */}

Show all pages

Show enabled only

Show disabled only

{/* Expand / Collapse all */} {searchResults === null && ( <>

Expand all

Collapse all

)} {/* Search */}
setSearchQuery(e.target.value)} className="h-8 pl-8 pr-8 text-sm" onKeyDown={(e) => { if (e.key === "Escape") { setSearchQuery(""); searchInputRef.current?.blur(); } }} /> {searching ? ( ) : ( searchQuery && ( ) )}
{/* Tree body */}
{searchResults !== null && (

{searchResults.length} result {searchResults.length !== 1 ? "s" : ""} for " {searchQuery}"

)} {displayPages.map((page) => ( ))} {displayPages.length === 0 && (

{searchResults !== null ? "No pages match your search" : "No catalog pages found"}

)}
); } /* ─── Tree Item ──────────────────────────────────────────── */ function TreeItem({ node, level, isSearchResult, defaultExpanded, }: { node: TreeNode; level: number; isSearchResult?: boolean; defaultExpanded?: boolean; }) { void isSearchResult; const { expandSignal, collapseSignal, filterMode, searchQuery, onToggleEnabled, onToggleVisible, onDeletePage, catalogType, } = useContext(TreeCtx); const router = useRouter(); const [expanded, setExpanded] = useState(false); const [children, setChildren] = useState([]); const [loading, setLoading] = useState(false); const [loaded, setLoaded] = useState(false); const hasChildren = node.childCount > 0; const loadChildren = useCallback(async () => { if (loaded || !hasChildren) return; setLoading(true); try { const catParam = catalogType === "bc" ? "&catalog=bc" : ""; const res = await adminFetch( `/api/admin/catalog/tree?parentId=${node.id}${catParam}`, ); if (res.ok) { const data = await res.json(); setChildren(data.pages || []); setLoaded(true); } } catch (error) { console.error("[CatalogTree] Failed to load children:", error); } finally { setLoading(false); } }, [node.id, loaded, hasChildren, catalogType]); // Auto-expand root level on mount useEffect(() => { if (defaultExpanded && level === 0 && hasChildren && !loaded) { loadChildren().then(() => setExpanded(true)); } }, [defaultExpanded, level, hasChildren, loaded, loadChildren]); // Expand all signal useEffect(() => { if (expandSignal > 0 && hasChildren) { if (!loaded) { loadChildren().then(() => setExpanded(true)); } else { setExpanded(true); } } }, [expandSignal, hasChildren, loaded, loadChildren]); // Collapse all signal useEffect(() => { if (collapseSignal > 0) { setExpanded(false); } }, [collapseSignal]); const handleToggle = () => { if (!expanded && !loaded) { loadChildren().then(() => setExpanded(true)); return; } setExpanded(!expanded); }; const isDisabled = node.enabled !== "1"; const isHidden = node.visible !== "1"; const isEmpty = node.itemCount === 0 && node.childCount === 0; const layoutColor = LAYOUT_COLORS[node.pageLayout] || "bg-muted text-muted-foreground"; const italianName = translateCaption(node.caption); const displayName = italianName ?? node.caption; return (
{/* biome-ignore lint/a11y/useSemanticElements: wrapped by Tooltip+ContextMenuTrigger asChild (Radix); a real button nests button-in-button when expanded */}
router.push( catalogType === "bc" ? `/admin/catalog/builder-club/${node.id}` : `/admin/catalog/${node.id}`, ) } onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); router.push( catalogType === "bc" ? `/admin/catalog/builder-club/${node.id}` : `/admin/catalog/${node.id}`, ); } }} > {/* Expand/collapse toggle */} {hasChildren ? ( ) : ( )} {/* Catalog icon */} {/* Caption link */} {italianName && ( )} {/* Quick toggle enabled on hover */}

{isDisabled ? "Enable" : "Disable"}

{/* Quick add subpage */}

Create subpage

{/* Status indicators */}
{isHidden && ( )} {isDisabled && ( OFF )} {node.itemCount > 0 && ( {node.itemCount} )} {/* Layout badge on hover */} {node.pageLayout.replace("default_3x3", "3x3")}

{node.caption}{" "} #{node.id}

Layout {node.pageLayout}
Items {node.itemCount}
Subpages {node.childCount}
Icon {node.iconImage}
Visible {node.visible === "1" ? "Yes" : "No"}
Enabled {node.enabled === "1" ? "Yes" : "No"}

Right-click for actions · Double-click to open

{/* Right-click context menu */} {node.caption} #{node.id} router.push( catalogType === "bc" ? `/admin/catalog/builder-club/${node.id}` : `/admin/catalog/${node.id}`, ) } > Edit Page navigator.clipboard.writeText(String(node.id))} > Copy ID onToggleEnabled(node)}> {node.enabled === "1" ? ( ) : ( )} {node.enabled === "1" ? "Disable" : "Enable"} onToggleVisible(node)}> {node.visible === "1" ? ( ) : ( )} {node.visible === "1" ? "Hide" : "Show"} router.push( catalogType === "bc" ? `/admin/catalog?catalog=bc&newPageParent=${node.id}` : `/admin/catalog?newPageParent=${node.id}`, ) } > Create Subpage onDeletePage(node)} > Delete
{/* Children */} {expanded && children.length > 0 && (() => { const filteredChildren = filterMode === "all" ? children : children.filter((c) => filterMode === "enabled" ? c.enabled === "1" : c.enabled !== "1", ); return filteredChildren.length > 0 ? (
{/* Tree guide line */}
{filteredChildren.map((child) => ( ))}
) : null; })()}
); }