Files
tm_panel/src/components/Layouts/sidebar/index.tsx
T
AmirReza Jamali 3276bc80e4 feat(sidebar): Add support for multi-level nested navigation
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.
2025-09-23 13:14:02 +03:30

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>
</>
);
}