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:
+2
-2
@@ -7,8 +7,8 @@ interface IProps {}
|
|||||||
const CreateCompanyCategoryPage = ({}: IProps) => {
|
const CreateCompanyCategoryPage = ({}: IProps) => {
|
||||||
const breadcrumbItems: BreadcrumbItem[] = [
|
const breadcrumbItems: BreadcrumbItem[] = [
|
||||||
{ href: "/", name: "Dashboard" },
|
{ href: "/", name: "Dashboard" },
|
||||||
{ href: "/companies/categories", name: "Company Categories" },
|
{ href: "/company-categories", name: "Company Categories" },
|
||||||
{ href: "/companies/categories/create", name: "Create Company Category" },
|
{ href: "/company-categories/create", name: "Create Company Category" },
|
||||||
];
|
];
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
+2
-2
@@ -15,8 +15,8 @@ const EditCompanyCategory = ({
|
|||||||
const { isLoading, data } = useCompanyCategoriesDetails(id);
|
const { isLoading, data } = useCompanyCategoriesDetails(id);
|
||||||
const breadcrumbItems: BreadcrumbItem[] = [
|
const breadcrumbItems: BreadcrumbItem[] = [
|
||||||
{ name: "Dashboard", href: "/" },
|
{ name: "Dashboard", href: "/" },
|
||||||
{ name: "Company Categories", href: "/companies/categories" },
|
{ name: "Company Categories", href: "/company-categories" },
|
||||||
{ name: "Edit Company Category", href: `/companies/categories/edit/${id}` },
|
{ name: "Edit Company Category", href: `/company-categories/edit/${id}` },
|
||||||
];
|
];
|
||||||
if (isLoading) return <Loading />;
|
if (isLoading) return <Loading />;
|
||||||
return (
|
return (
|
||||||
+1
-1
@@ -4,7 +4,7 @@ import CompanyCategoriesTable from "@/components/Tables/companies/company-catego
|
|||||||
const CompanyCategoryList = () => {
|
const CompanyCategoryList = () => {
|
||||||
const breadcrumbItems = [
|
const breadcrumbItems = [
|
||||||
{ name: "Dashboard", href: "/" },
|
{ name: "Dashboard", href: "/" },
|
||||||
{ name: "Company Categories", href: "/companies/categories" },
|
{ name: "Company Categories", href: "/company-categories" },
|
||||||
];
|
];
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import * as Icons from "../icons";
|
import * as Icons from "../icons";
|
||||||
|
|
||||||
const navSubItemSchema: z.ZodType<any> = z.lazy(() =>
|
const navSubItemSchema: z.ZodType<any> = z.lazy(() =>
|
||||||
z.object({
|
z.object({
|
||||||
title: z.string(),
|
title: z.string(),
|
||||||
@@ -36,13 +37,20 @@ export const NAV_DATA: NavData = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Manage Companies",
|
title: "Manage Companies",
|
||||||
|
url: "",
|
||||||
icon: Icons.Building,
|
icon: Icons.Building,
|
||||||
items: [
|
items: [
|
||||||
{ Icons: Icons.Building, url: "/companies", title: "Companies" },
|
|
||||||
{
|
{
|
||||||
Icons: Icons.Building,
|
icon: Icons.Building,
|
||||||
url: "/companies/categories",
|
url: "/companies",
|
||||||
|
title: "Companies",
|
||||||
|
items: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Icons.Building,
|
||||||
|
url: "/company-categories",
|
||||||
title: "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",
|
|
||||||
// },
|
|
||||||
// ],
|
|
||||||
// },
|
|
||||||
// ],
|
|
||||||
// },
|
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import { NAV_DATA, NavSubItem } from "./data";
|
|||||||
import { ArrowLeftIcon, ChevronUp } from "./icons";
|
import { ArrowLeftIcon, ChevronUp } from "./icons";
|
||||||
import { MenuItem } from "./menu-item";
|
import { MenuItem } from "./menu-item";
|
||||||
import { useSidebarContext } from "./sidebar-context";
|
import { useSidebarContext } from "./sidebar-context";
|
||||||
import Image from "next/image";
|
|
||||||
|
|
||||||
export function Sidebar() {
|
export function Sidebar() {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
@@ -17,35 +16,35 @@ export function Sidebar() {
|
|||||||
const [expandedItems, setExpandedItems] = useState<string[]>([]);
|
const [expandedItems, setExpandedItems] = useState<string[]>([]);
|
||||||
|
|
||||||
const toggleExpanded = (title: string) => {
|
const toggleExpanded = (title: string) => {
|
||||||
// setExpandedItems((prev) => (prev.includes(title) ? [] : [title]));
|
|
||||||
|
|
||||||
// Uncomment the following line to enable multiple expanded items
|
|
||||||
setExpandedItems((prev) =>
|
setExpandedItems((prev) =>
|
||||||
prev.includes(title) ? prev.filter((t) => t !== title) : [...prev, title],
|
prev.includes(title) ? prev.filter((t) => t !== title) : [...prev, title],
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Keep collapsible open, when it's subpage is active
|
const newExpandedItems: string[] = [];
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Break the loop
|
NAV_DATA.forEach((section) => {
|
||||||
return true;
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Mobile Overlay */}
|
|
||||||
{isMobile && isOpen && (
|
{isMobile && isOpen && (
|
||||||
<div
|
<div
|
||||||
className="fixed inset-0 z-40 bg-black/50 transition-opacity duration-300"
|
className="fixed inset-0 z-40 bg-black/50 transition-opacity duration-300"
|
||||||
@@ -86,7 +85,6 @@ export function Sidebar() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Navigation */}
|
|
||||||
<div className="custom-scrollbar mt-6 flex-1 overflow-y-auto pr-3 min-[850px]:mt-10">
|
<div className="custom-scrollbar mt-6 flex-1 overflow-y-auto pr-3 min-[850px]:mt-10">
|
||||||
{NAV_DATA.map((section) => (
|
{NAV_DATA.map((section) => (
|
||||||
<div key={section.label} className="mb-6">
|
<div key={section.label} className="mb-6">
|
||||||
@@ -101,9 +99,17 @@ export function Sidebar() {
|
|||||||
{item.items.length ? (
|
{item.items.length ? (
|
||||||
<div>
|
<div>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
isActive={item.items.some(
|
isActive={
|
||||||
({ url }: { url: string }) => url === pathname,
|
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)}
|
onClick={() => toggleExpanded(item.title)}
|
||||||
>
|
>
|
||||||
<item.icon
|
<item.icon
|
||||||
@@ -133,7 +139,11 @@ export function Sidebar() {
|
|||||||
<MenuItem
|
<MenuItem
|
||||||
as="link"
|
as="link"
|
||||||
href={subItem.url}
|
href={subItem.url}
|
||||||
isActive={pathname === subItem.url}
|
isActive={
|
||||||
|
pathname === subItem.url ||
|
||||||
|
(pathname.startsWith(subItem.url) &&
|
||||||
|
subItem.url !== "/")
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<span>{subItem.title}</span>
|
<span>{subItem.title}</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
@@ -150,12 +160,17 @@ export function Sidebar() {
|
|||||||
: "/" +
|
: "/" +
|
||||||
item.title.toLowerCase().split(" ").join("-");
|
item.title.toLowerCase().split(" ").join("-");
|
||||||
|
|
||||||
|
const isActive =
|
||||||
|
href === "/"
|
||||||
|
? pathname === href
|
||||||
|
: pathname.startsWith(href);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
className="flex items-center gap-3 py-3"
|
className="flex items-center gap-3 py-3"
|
||||||
as="link"
|
as="link"
|
||||||
href={href}
|
href={href}
|
||||||
isActive={pathname === href}
|
isActive={isActive}
|
||||||
>
|
>
|
||||||
<item.icon
|
<item.icon
|
||||||
className="size-6 shrink-0"
|
className="size-6 shrink-0"
|
||||||
|
|||||||
Reference in New Issue
Block a user