From 3276bc80e42d4c15bfd4729dc110981b02877bba Mon Sep 17 00:00:00 2001 From: AmirReza Jamali Date: Tue, 23 Sep 2025 13:14:02 +0330 Subject: [PATCH] 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. --- .../companies/create/page.tsx | 0 .../companies/edit/[id]/page.tsx | 0 src/app/{ => (companies)}/companies/page.tsx | 0 .../company-categories}/create/page.tsx | 4 +- .../company-categories}/edit/[id]/page.tsx | 4 +- .../company-categories}/page.tsx | 2 +- src/components/Layouts/sidebar/data/index.ts | 115 ++---------------- src/components/Layouts/sidebar/index.tsx | 61 ++++++---- 8 files changed, 54 insertions(+), 132 deletions(-) rename src/app/{ => (companies)}/companies/create/page.tsx (100%) rename src/app/{ => (companies)}/companies/edit/[id]/page.tsx (100%) rename src/app/{ => (companies)}/companies/page.tsx (100%) rename src/app/{companies/categories => (companies)/company-categories}/create/page.tsx (78%) rename src/app/{companies/categories => (companies)/company-categories}/edit/[id]/page.tsx (85%) rename src/app/{companies/categories => (companies)/company-categories}/page.tsx (85%) diff --git a/src/app/companies/create/page.tsx b/src/app/(companies)/companies/create/page.tsx similarity index 100% rename from src/app/companies/create/page.tsx rename to src/app/(companies)/companies/create/page.tsx diff --git a/src/app/companies/edit/[id]/page.tsx b/src/app/(companies)/companies/edit/[id]/page.tsx similarity index 100% rename from src/app/companies/edit/[id]/page.tsx rename to src/app/(companies)/companies/edit/[id]/page.tsx diff --git a/src/app/companies/page.tsx b/src/app/(companies)/companies/page.tsx similarity index 100% rename from src/app/companies/page.tsx rename to src/app/(companies)/companies/page.tsx diff --git a/src/app/companies/categories/create/page.tsx b/src/app/(companies)/company-categories/create/page.tsx similarity index 78% rename from src/app/companies/categories/create/page.tsx rename to src/app/(companies)/company-categories/create/page.tsx index 8f707a6..63ea62d 100644 --- a/src/app/companies/categories/create/page.tsx +++ b/src/app/(companies)/company-categories/create/page.tsx @@ -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 ( <> diff --git a/src/app/companies/categories/edit/[id]/page.tsx b/src/app/(companies)/company-categories/edit/[id]/page.tsx similarity index 85% rename from src/app/companies/categories/edit/[id]/page.tsx rename to src/app/(companies)/company-categories/edit/[id]/page.tsx index 145abe1..fa76295 100644 --- a/src/app/companies/categories/edit/[id]/page.tsx +++ b/src/app/(companies)/company-categories/edit/[id]/page.tsx @@ -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 ; return ( diff --git a/src/app/companies/categories/page.tsx b/src/app/(companies)/company-categories/page.tsx similarity index 85% rename from src/app/companies/categories/page.tsx rename to src/app/(companies)/company-categories/page.tsx index ad64413..093a04c 100644 --- a/src/app/companies/categories/page.tsx +++ b/src/app/(companies)/company-categories/page.tsx @@ -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 ( <> diff --git a/src/components/Layouts/sidebar/data/index.ts b/src/components/Layouts/sidebar/data/index.ts index 00bbcf2..05bf2e5 100644 --- a/src/components/Layouts/sidebar/data/index.ts +++ b/src/components/Layouts/sidebar/data/index.ts @@ -1,5 +1,6 @@ import { z } from "zod"; import * as Icons from "../icons"; + const navSubItemSchema: z.ZodType = 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", - // }, - // ], - // }, - // ], - // }, ]; diff --git a/src/components/Layouts/sidebar/index.tsx b/src/components/Layouts/sidebar/index.tsx index 54ef8cb..13b7392 100644 --- a/src/components/Layouts/sidebar/index.tsx +++ b/src/components/Layouts/sidebar/index.tsx @@ -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([]); 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 && (
- {/* Navigation */}
{NAV_DATA.map((section) => (
@@ -101,9 +99,17 @@ export function Sidebar() { {item.items.length ? (
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)} > {subItem.title} @@ -150,12 +160,17 @@ export function Sidebar() { : "/" + item.title.toLowerCase().split(" ").join("-"); + const isActive = + href === "/" + ? pathname === href + : pathname.startsWith(href); + return (