"use client"; import Link from "next/link"; import { usePathname } from "next/navigation"; import { ChevronRight } from "lucide-react"; import type { TFunction } from "i18next"; import { useEffect, useMemo, useState, type ReactElement } from "react"; import { useTranslation } from "react-i18next"; import { SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarMenuSub, SidebarMenuSubButton, SidebarMenuSubItem, useSidebar, } from "@/components/ui/sidebar"; import { ADMIN_NAV_GROUP_ICON, ADMIN_NAV_GROUP_ORDER, groupAdminNavItems, } from "@/lib/admin-nav-groups"; import { adminNavLabel } from "@/lib/admin-nav-label"; import { cn } from "@/lib/utils"; import { resolveAdminNavIcon } from "@/modules/_config/admin-nav-icons"; import { ADMIN_BASE, type AdminNavGroup, type AdminNavItem } from "@/modules/_config/admin-nav"; const NAV_BTN = "h-8 gap-2 px-2.5 py-0 text-[13px] leading-snug font-normal text-sidebar-foreground/90 hover:text-sidebar-accent-foreground [&_svg]:size-4"; const NAV_ACTIVE = "data-active:bg-red-600 data-active:text-white data-active:font-medium data-active:shadow-sm"; const SUB_NAV = "h-8 min-h-8 gap-2 rounded-sm px-2.5 py-0 text-sm leading-snug font-normal text-sidebar-foreground/90 hover:text-sidebar-accent-foreground data-[size=md]:text-sm data-[size=sm]:text-sm [&>span]:text-sm [&_svg]:size-4"; function isActive( pathname: string, item: { href: string; activeMatchPrefix?: string; segment?: string }, ): boolean { const { href, activeMatchPrefix, segment } = item; const prefix = activeMatchPrefix ?? href; if (prefix === ADMIN_BASE || prefix === `${ADMIN_BASE}/`) { return pathname === ADMIN_BASE || pathname === `${ADMIN_BASE}/`; } if (segment === "settings") { return pathname === href; } return pathname === prefix || pathname.startsWith(`${prefix}/`); } function defaultOpenGroups( groups: { group: AdminNavGroup; items: AdminNavItem[] }[], pathname: string, ): Record { const open = Object.fromEntries( ADMIN_NAV_GROUP_ORDER.map((g) => [g, true]), ) as Record; for (const { group, items } of groups) { if (items.some((item) => isActive(pathname, item))) { open[group] = true; } } return open; } function NavLeaf({ item, pathname, t, }: { item: AdminNavItem; pathname: string; t: TFunction; }): ReactElement { const Icon = resolveAdminNavIcon(item.segment); const active = isActive(pathname, item); const label = adminNavLabel(item.segment, t, item.label); return ( } className={cn(NAV_BTN, NAV_ACTIVE)} > {label} ); } function NavSubLeaf({ item, pathname, t, }: { item: AdminNavItem; pathname: string; t: TFunction; }): ReactElement { const Icon = resolveAdminNavIcon(item.segment); const active = isActive(pathname, item); const label = adminNavLabel(item.segment, t, item.label); return ( } className={cn(SUB_NAV, NAV_ACTIVE)} > {label} ); } function NavCollapsibleGroup({ group, items, pathname, open, onToggle, t, }: { group: AdminNavGroup; items: AdminNavItem[]; pathname: string; open: boolean; onToggle: () => void; t: TFunction; }): ReactElement { const GroupIcon = ADMIN_NAV_GROUP_ICON[group]; const groupLabel = t(`sidebar.group.${group}`, { ns: "common", defaultValue: group }); const hasActiveChild = items.some((item) => isActive(pathname, item)); return ( {groupLabel} {open ? ( {items.map((item) => ( ))} ) : null} ); } export function AdminSidebarNav({ items, }: { items: readonly AdminNavItem[]; }): ReactElement { const { t } = useTranslation("common"); const pathname = usePathname(); const { state } = useSidebar(); const navGroups = useMemo(() => groupAdminNavItems(items), [items]); const flatItems = useMemo(() => navGroups.flatMap((g) => g.items), [navGroups]); const [openGroups, setOpenGroups] = useState>(() => defaultOpenGroups(navGroups, pathname), ); useEffect(() => { setOpenGroups((prev) => { const next = { ...prev }; let changed = false; for (const { group, items: groupItems } of navGroups) { if (groupItems.some((item) => isActive(pathname, item)) && !next[group]) { next[group] = true; changed = true; } } return changed ? next : prev; }); }, [pathname, navGroups]); if (state === "collapsed") { return ( {flatItems.map((item) => ( ))} ); } const overview = navGroups.find((g) => g.group === "overview"); const collapsible = navGroups.filter((g) => g.group !== "overview"); return ( {overview?.items.map((item) => ( ))} {collapsible.map(({ group, items: groupItems }) => ( setOpenGroups((prev) => ({ ...prev, [group]: !(prev[group] ?? true), })) } t={t} /> ))} ); } export function AdminSidebarNavSkeleton(): ReactElement { const { t } = useTranslation("common"); const widths = ["68%", "74%", "58%", "70%", "62%"] as const; return (
{widths.map((width, i) => (
))} {t("auth.checking")}
); }