"use client"; import { ChevronDown, ChevronsDownUp, ChevronsUpDown, Search, X, } from "lucide-react"; import { usePathname } from "next/navigation"; import { useTranslations } from "next-intl"; import { useCallback, useEffect, useMemo, useState } from "react"; import { AdminNavLink } from "@/components/admin/admin-nav-link"; import type { AdminNavItem } from "@/lib/admin-nav"; import type { AdminNavConfig } from "@/lib/admin-nav-config"; import { visibleAdminNavigation } from "@/lib/admin-navigation"; import { cn } from "@/lib/utils"; const STORAGE_KEY = "admin-nav-collapsed"; function itemIsActive(pathname: string, item: AdminNavItem): boolean { if ( item.matchExcludePrefixes?.some( (p) => pathname === p || pathname.startsWith(`${p}/`), ) ) { return false; } if (item.href === "/admin") return pathname === "/admin"; if (item.matchPrefixes?.length) { return item.matchPrefixes.some( (p) => pathname === p || pathname.startsWith(`${p}/`), ); } return pathname === item.href || pathname.startsWith(`${item.href}/`); } function groupHasActive(pathname: string, items: AdminNavItem[]): boolean { return items.some((item) => itemIsActive(pathname, item)); } function readCollapsed(): Record { if (typeof window === "undefined") return {}; try { const raw = localStorage.getItem(STORAGE_KEY); if (!raw) return {}; const parsed = JSON.parse(raw) as unknown; if (!parsed || typeof parsed !== "object") return {}; return parsed as Record; } catch { return {}; } } function persistCollapsed(next: Record) { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(next)); } catch { /* ignore quota */ } } export function AdminSidebarNav({ allowedPermissions = [], isSuperAdmin = false, navConfig = {}, }: { /** ACL slugs the staff member holds (ignored when isSuperAdmin). */ allowedPermissions?: string[]; isSuperAdmin?: boolean; /** Owner overlay: hide + reorder groups/items (from website_settings). */ navConfig?: AdminNavConfig; }) { const t = useTranslations("pages.admin.nav"); const pathname = usePathname() ?? ""; const [collapsed, setCollapsed] = useState>({}); const [hydrated, setHydrated] = useState(false); const [query, setQuery] = useState(""); const groups = useMemo( () => visibleAdminNavigation({ allowedPermissions, isSuperAdmin, navConfig }), [allowedPermissions, isSuperAdmin, navConfig], ); const search = query.trim().toLocaleLowerCase(); const visibleGroups = useMemo( () => groups .map((group) => ({ ...group, items: group.items.filter( (item) => !search || t(group.labelKey).toLocaleLowerCase().includes(search) || t(item.labelKey).toLocaleLowerCase().includes(search), ), })) .filter((group) => group.items.length > 0), [groups, search, t], ); const allKeys = useMemo(() => groups.map((g) => g.labelKey), [groups]); useEffect(() => { const saved = readCollapsed(); setCollapsed( Object.fromEntries( groups.map((group) => [ group.labelKey, typeof saved[group.labelKey] === "boolean" ? saved[group.labelKey] : true, ]), ), ); setHydrated(true); }, [groups]); // Keep the group that contains the current page open. useEffect(() => { if (!hydrated) return; const activeKeys = groups .filter((g) => groupHasActive(pathname, g.items)) .map((g) => g.labelKey); if (activeKeys.length === 0) return; setCollapsed((prev) => { let changed = false; const next = { ...prev }; for (const key of activeKeys) { if (next[key]) { next[key] = false; changed = true; } } if (!changed) return prev; persistCollapsed(next); return next; }); }, [pathname, hydrated, groups]); const toggle = useCallback((labelKey: string) => { setCollapsed((prev) => { const next = { ...prev, [labelKey]: !prev[labelKey] }; persistCollapsed(next); return next; }); }, []); const expandAll = useCallback(() => { const next: Record = {}; for (const key of allKeys) next[key] = false; persistCollapsed(next); setCollapsed(next); }, [allKeys]); const collapseAll = useCallback(() => { const next: Record = {}; for (const key of allKeys) next[key] = true; // Keep the active route's group open so you never lose context. for (const g of groups) { if (groupHasActive(pathname, g.items)) next[g.labelKey] = false; } persistCollapsed(next); setCollapsed(next); }, [allKeys, groups, pathname]); const allExpanded = hydrated && allKeys.every((k) => !collapsed[k]); const canCollapseMore = hydrated && allKeys.some((k) => { const g = groups.find((x) => x.labelKey === k); if (g && groupHasActive(pathname, g.items)) return false; return !collapsed[k]; }); return ( ); }