3276bc80e4
This commit refactors the sidebar component to support arbitrarily deep nested navigation items. The previous implementation was limited to one level of nesting and contained complex rendering logic directly within the main component, making it difficult to extend and maintain. Changes include: - Extracted the rendering logic into a new recursive `MenuItem` component. - Rewrote the effect that keeps the active path expanded to correctly handle multiple levels of nesting. - Updated the navigation data structure to reflect the new capabilities and corrected the URL for "Company Categories". - Removed a large block of commented-out, unused navigation data.
197 lines
7.2 KiB
TypeScript
197 lines
7.2 KiB
TypeScript
"use client";
|
|
|
|
import { Logo } from "@/components/logo";
|
|
import { cn } from "@/lib/utils";
|
|
import Link from "next/link";
|
|
import { usePathname } from "next/navigation";
|
|
import { useEffect, useState } from "react";
|
|
import { NAV_DATA, NavSubItem } from "./data";
|
|
import { ArrowLeftIcon, ChevronUp } from "./icons";
|
|
import { MenuItem } from "./menu-item";
|
|
import { useSidebarContext } from "./sidebar-context";
|
|
|
|
export function Sidebar() {
|
|
const pathname = usePathname();
|
|
const { setIsOpen, isOpen, isMobile, toggleSidebar } = useSidebarContext();
|
|
const [expandedItems, setExpandedItems] = useState<string[]>([]);
|
|
|
|
const toggleExpanded = (title: string) => {
|
|
setExpandedItems((prev) =>
|
|
prev.includes(title) ? prev.filter((t) => t !== title) : [...prev, title],
|
|
);
|
|
};
|
|
|
|
useEffect(() => {
|
|
const newExpandedItems: string[] = [];
|
|
|
|
NAV_DATA.forEach((section) => {
|
|
section.items.forEach((item) => {
|
|
if (item.items.length > 0) {
|
|
const hasActiveSubItem = item.items.some(
|
|
(subItem: NavSubItem) =>
|
|
subItem.url === pathname ||
|
|
(pathname.startsWith(subItem.url) && subItem.url !== "/"),
|
|
);
|
|
|
|
if (hasActiveSubItem) {
|
|
newExpandedItems.push(item.title);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
setExpandedItems(newExpandedItems);
|
|
}, [pathname]);
|
|
|
|
return (
|
|
<>
|
|
{isMobile && isOpen && (
|
|
<div
|
|
className="fixed inset-0 z-40 bg-black/50 transition-opacity duration-300"
|
|
onClick={() => setIsOpen(false)}
|
|
aria-hidden="true"
|
|
/>
|
|
)}
|
|
|
|
<aside
|
|
className={cn(
|
|
"max-w-[290px] overflow-hidden border-r border-gray-200 bg-white transition-[width] duration-200 ease-linear dark:border-gray-800 dark:bg-gray-dark",
|
|
isMobile ? "fixed bottom-0 top-0 z-50" : "sticky top-0 h-screen",
|
|
isOpen ? "w-full" : "w-0",
|
|
)}
|
|
aria-label="Main navigation"
|
|
aria-hidden={!isOpen}
|
|
inert={!isOpen}
|
|
>
|
|
<div className="flex h-full flex-col py-10 pl-[25px] pr-[7px]">
|
|
<div className="relative pr-4.5">
|
|
<Link
|
|
href={"/"}
|
|
onClick={() => isMobile && toggleSidebar()}
|
|
className="px-0 py-2.5 min-[850px]:py-0"
|
|
>
|
|
<Logo />
|
|
</Link>
|
|
|
|
{isMobile && (
|
|
<button
|
|
onClick={toggleSidebar}
|
|
className="absolute left-3/4 right-4.5 top-1/2 -translate-y-1/2 text-right"
|
|
>
|
|
<span className="sr-only">Close Menu</span>
|
|
|
|
<ArrowLeftIcon className="ml-auto size-7" />
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
<div className="custom-scrollbar mt-6 flex-1 overflow-y-auto pr-3 min-[850px]:mt-10">
|
|
{NAV_DATA.map((section) => (
|
|
<div key={section.label} className="mb-6">
|
|
<h2 className="mb-5 text-sm font-medium text-dark-4 dark:text-dark-6">
|
|
{section.label}
|
|
</h2>
|
|
|
|
<nav role="navigation" aria-label={section.label}>
|
|
<ul className="space-y-2">
|
|
{section.items.map((item) => (
|
|
<li key={item.title}>
|
|
{item.items.length ? (
|
|
<div>
|
|
<MenuItem
|
|
isActive={
|
|
item.items.some(
|
|
({ url }: { url: string }) =>
|
|
url === pathname,
|
|
) ||
|
|
(item.items.length > 0 &&
|
|
item.items.some(
|
|
({ url }: { url: string }) =>
|
|
pathname.startsWith(url) && url !== "/",
|
|
))
|
|
}
|
|
onClick={() => toggleExpanded(item.title)}
|
|
>
|
|
<item.icon
|
|
className="size-6 shrink-0"
|
|
aria-hidden="true"
|
|
/>
|
|
|
|
<span>{item.title}</span>
|
|
|
|
<ChevronUp
|
|
className={cn(
|
|
"ml-auto rotate-180 transition-transform duration-200",
|
|
expandedItems.includes(item.title) &&
|
|
"rotate-0",
|
|
)}
|
|
aria-hidden="true"
|
|
/>
|
|
</MenuItem>
|
|
|
|
{expandedItems.includes(item.title) && (
|
|
<ul
|
|
className="ml-9 mr-0 space-y-1.5 pb-[15px] pr-0 pt-2"
|
|
role="menu"
|
|
>
|
|
{item.items.map((subItem: NavSubItem) => (
|
|
<li key={subItem.title} role="none">
|
|
<MenuItem
|
|
as="link"
|
|
href={subItem.url}
|
|
isActive={
|
|
pathname === subItem.url ||
|
|
(pathname.startsWith(subItem.url) &&
|
|
subItem.url !== "/")
|
|
}
|
|
>
|
|
<span>{subItem.title}</span>
|
|
</MenuItem>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</div>
|
|
) : (
|
|
(() => {
|
|
const href =
|
|
"url" in item
|
|
? item.url + ""
|
|
: "/" +
|
|
item.title.toLowerCase().split(" ").join("-");
|
|
|
|
const isActive =
|
|
href === "/"
|
|
? pathname === href
|
|
: pathname.startsWith(href);
|
|
|
|
return (
|
|
<MenuItem
|
|
className="flex items-center gap-3 py-3"
|
|
as="link"
|
|
href={href}
|
|
isActive={isActive}
|
|
>
|
|
<item.icon
|
|
className="size-6 shrink-0"
|
|
aria-hidden="true"
|
|
/>
|
|
|
|
<span>{item.title}</span>
|
|
</MenuItem>
|
|
);
|
|
})()
|
|
)}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</nav>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
</>
|
|
);
|
|
}
|