import { type MouseEvent, useCallback, useEffect, useMemo, useState } from "react"; import { ChevronDown, ChevronRight } from "lucide-react"; import { cn } from "@/lib/utils"; import type { SidebarItem, SidebarSection } from "./types"; const EDR_LOGO = "/assets/logo.svg"; export interface FreightSidebarProps { sections: SidebarSection[]; activeHref?: string; onNavigate?: (href: string) => void; } const sidebarItemKey = (item: SidebarItem, parentKey: string) => item.href ?? `${parentKey}::${item.label}`; const collectSidebarHrefs = (items: SidebarItem[]): string[] => items.flatMap((item) => { const hrefs: string[] = []; if (item.href) hrefs.push(item.href.toLowerCase()); if (item.children?.length) hrefs.push(...collectSidebarHrefs(item.children)); return hrefs; }); const flattenSectionItems = (sections: SidebarSection[]) => sections.flatMap((section) => section.items); const FreightSidebar = ({ sections, activeHref, onNavigate }: FreightSidebarProps) => { const items = useMemo(() => flattenSectionItems(sections), [sections]); const activePath = activeHref?.toLowerCase() ?? ""; const isHrefActive = useCallback( (href: string) => { const normalized = href.toLowerCase(); return activePath === normalized || activePath.startsWith(`${normalized}/`); }, [activePath], ); const branchContainsActive = useCallback( (branch: SidebarItem[]) => collectSidebarHrefs(branch).some((href) => isHrefActive(href)), [isHrefActive], ); const defaultExpanded = useMemo(() => { const acc: Record = {}; const walk = (entries: SidebarItem[], parentKey: string) => { for (const entry of entries) { if (!entry.children?.length) continue; const key = sidebarItemKey(entry, parentKey); acc[key] = branchContainsActive(entry.children) || (entry.href ? isHrefActive(entry.href) : false); walk(entry.children, key); } }; for (const item of items) { if (!item.children?.length) continue; const key = item.href ?? item.label; acc[key] = activePath === key.toLowerCase() || activePath.startsWith(`${key.toLowerCase()}/`) || branchContainsActive(item.children); walk(item.children, key); } return acc; }, [activePath, branchContainsActive, isHrefActive, items]); const [expanded, setExpanded] = useState>(defaultExpanded); useEffect(() => { setExpanded((current) => ({ ...defaultExpanded, ...current })); }, [defaultExpanded]); const navigateTo = (event: MouseEvent, href: string) => { if (onNavigate) { event.preventDefault(); onNavigate(href); } }; const toggleExpanded = (key: string) => { setExpanded((current) => ({ ...current, [key]: !current[key] })); }; const navLinkClass = (active: boolean, depth: number) => cn( "flex items-center justify-between rounded-md px-3 py-2.5 text-base font-medium leading-snug transition-colors", active ? "bg-primary text-primary-foreground shadow-sm" : "text-gray-900 hover:bg-gray-100", depth > 0 && "text-[15px]", ); const iconClass = (active: boolean, sectionActive: boolean) => cn( "flex h-5 w-5 shrink-0 items-center justify-center [&_svg]:h-5 [&_svg]:w-5", active ? "text-primary-foreground" : sectionActive ? "text-gray-900" : "text-gray-900", ); const renderNavBranch = (children: SidebarItem[], depth: number, parentKey: string) => children.map((child) => { const key = sidebarItemKey(child, parentKey); const isGroup = Boolean(child.children?.length) && !child.href; if (isGroup) { const isOpen = expanded[key] ?? false; const groupActive = branchContainsActive(child.children!); return (
{isOpen ? (
{renderNavBranch(child.children!, depth + 1, key)}
) : null}
); } if (!child.href) return null; const childHref = child.href.toLowerCase(); const childActiveHref = isHrefActive(childHref); return ( navigateTo(event, child.href!)} aria-current={childActiveHref ? "page" : undefined} className={navLinkClass(childActiveHref, depth)} > {child.label} ); }); const renderTopLevelItem = (item: SidebarItem) => { if (!item.href) return null; const hasChildren = Boolean(item.children?.length); const itemHref = item.href.toLowerCase(); const childActive = hasChildren ? branchContainsActive(item.children!) : false; const isCurrentItem = hasChildren ? activePath === itemHref : isHrefActive(itemHref); const isSectionActive = childActive && !isCurrentItem; const isActive = isCurrentItem || isSectionActive; const isOpen = expanded[item.href] ?? false; const leafActive = isCurrentItem && !hasChildren; return (
navigateTo(event, item.href!)} aria-current={isCurrentItem ? "page" : undefined} className="flex min-w-0 flex-1 items-center gap-3 px-3 py-2.5 text-base font-medium leading-snug" > {item.icon ? ( {item.icon} ) : null} {item.label} {hasChildren ? ( ) : ( )}
{hasChildren && isOpen ? (
{renderNavBranch(item.children!, 0, item.href)}
) : null}
); }; return ( ); }; export default FreightSidebar;