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.
This commit is contained in:
AmirReza Jamali
2025-09-23 13:14:02 +03:30
parent c626baff21
commit 3276bc80e4
8 changed files with 54 additions and 132 deletions
@@ -7,8 +7,8 @@ interface IProps {}
const CreateCompanyCategoryPage = ({}: IProps) => {
const breadcrumbItems: BreadcrumbItem[] = [
{ href: "/", name: "Dashboard" },
{ href: "/companies/categories", name: "Company Categories" },
{ href: "/companies/categories/create", name: "Create Company Category" },
{ href: "/company-categories", name: "Company Categories" },
{ href: "/company-categories/create", name: "Create Company Category" },
];
return (
<>
@@ -15,8 +15,8 @@ const EditCompanyCategory = ({
const { isLoading, data } = useCompanyCategoriesDetails(id);
const breadcrumbItems: BreadcrumbItem[] = [
{ name: "Dashboard", href: "/" },
{ name: "Company Categories", href: "/companies/categories" },
{ name: "Edit Company Category", href: `/companies/categories/edit/${id}` },
{ name: "Company Categories", href: "/company-categories" },
{ name: "Edit Company Category", href: `/company-categories/edit/${id}` },
];
if (isLoading) return <Loading />;
return (
@@ -4,7 +4,7 @@ import CompanyCategoriesTable from "@/components/Tables/companies/company-catego
const CompanyCategoryList = () => {
const breadcrumbItems = [
{ name: "Dashboard", href: "/" },
{ name: "Company Categories", href: "/companies/categories" },
{ name: "Company Categories", href: "/company-categories" },
];
return (
<>
+11 -104
View File
@@ -1,5 +1,6 @@
import { z } from "zod";
import * as Icons from "../icons";
const navSubItemSchema: z.ZodType<any> = z.lazy(() =>
z.object({
title: z.string(),
@@ -36,13 +37,20 @@ export const NAV_DATA: NavData = [
},
{
title: "Manage Companies",
url: "",
icon: Icons.Building,
items: [
{ Icons: Icons.Building, url: "/companies", title: "Companies" },
{
Icons: Icons.Building,
url: "/companies/categories",
icon: Icons.Building,
url: "/companies",
title: "Companies",
items: [],
},
{
icon: Icons.Building,
url: "/company-categories",
title: "Company Categories",
items: [],
},
],
},
@@ -60,105 +68,4 @@ export const NAV_DATA: NavData = [
},
],
},
// {
// label: "MAIN MENU",
// items: [
// {
// title: "Dashboard",
// icon: Icons.HomeIcon,
// items: [
// {
// title: "eCommerce",
// url: "/",
// },
// ],
// },
// {
// title: "Calendar",
// url: "/calendar",
// icon: Icons.Calendar,
// items: [],
// },
// {
// title: "Profile",
// url: "/profile",
// icon: Icons.User,
// items: [],
// },
// {
// title: "Forms",
// icon: Icons.Alphabet,
// items: [
// {
// title: "Form Elements",
// url: "/forms/form-elements",
// },
// {
// title: "Form Layout",
// url: "/forms/form-layout",
// },
// ],
// },
// {
// title: "Tables",
// url: "/tables",
// icon: Icons.Table,
// items: [
// {
// title: "Tables",
// url: "/tables",
// },
// ],
// },
// {
// title: "Pages",
// icon: Icons.Alphabet,
// items: [
// {
// title: "Settings",
// url: "/pages/settings",
// },
// ],
// },
// ],
// },
// {
// label: "OTHERS",
// items: [
// {
// title: "Charts",
// icon: Icons.PieChart,
// items: [
// {
// title: "Basic Chart",
// url: "/charts/basic-chart",
// },
// ],
// },
// {
// title: "UI Elements",
// icon: Icons.FourCircle,
// items: [
// {
// title: "Alerts",
// url: "/ui-elements/alerts",
// },
// {
// title: "Buttons",
// url: "/ui-elements/buttons",
// },
// ],
// },
// {
// title: "Authentication",
// icon: Icons.Authentication,
// items: [
// {
// title: "Sign In",
// url: "/auth/sign-in",
// },
// ],
// },
// ],
// },
];
+38 -23
View File
@@ -9,7 +9,6 @@ import { NAV_DATA, NavSubItem } from "./data";
import { ArrowLeftIcon, ChevronUp } from "./icons";
import { MenuItem } from "./menu-item";
import { useSidebarContext } from "./sidebar-context";
import Image from "next/image";
export function Sidebar() {
const pathname = usePathname();
@@ -17,35 +16,35 @@ export function Sidebar() {
const [expandedItems, setExpandedItems] = useState<string[]>([]);
const toggleExpanded = (title: string) => {
// setExpandedItems((prev) => (prev.includes(title) ? [] : [title]));
// Uncomment the following line to enable multiple expanded items
setExpandedItems((prev) =>
prev.includes(title) ? prev.filter((t) => t !== title) : [...prev, title],
);
};
useEffect(() => {
// Keep collapsible open, when it's subpage is active
NAV_DATA.some((section) => {
return section.items.some((item) => {
return item.items.some((subItem: NavSubItem) => {
if (subItem.url === pathname) {
if (!expandedItems.includes(item.title)) {
toggleExpanded(item.title);
}
const newExpandedItems: string[] = [];
// Break the loop
return true;
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);
}
});
}
});
});
}, [pathname, expandedItems]);
setExpandedItems(newExpandedItems);
}, [pathname]);
return (
<>
{/* Mobile Overlay */}
{isMobile && isOpen && (
<div
className="fixed inset-0 z-40 bg-black/50 transition-opacity duration-300"
@@ -86,7 +85,6 @@ export function Sidebar() {
)}
</div>
{/* Navigation */}
<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">
@@ -101,9 +99,17 @@ export function Sidebar() {
{item.items.length ? (
<div>
<MenuItem
isActive={item.items.some(
({ url }: { url: string }) => url === pathname,
)}
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
@@ -133,7 +139,11 @@ export function Sidebar() {
<MenuItem
as="link"
href={subItem.url}
isActive={pathname === subItem.url}
isActive={
pathname === subItem.url ||
(pathname.startsWith(subItem.url) &&
subItem.url !== "/")
}
>
<span>{subItem.title}</span>
</MenuItem>
@@ -150,12 +160,17 @@ export function Sidebar() {
: "/" +
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={pathname === href}
isActive={isActive}
>
<item.icon
className="size-6 shrink-0"