Skip to content

Commit 8dead68

Browse files
committed
Modify sidebar to ba able to have submenus
1 parent b4712e3 commit 8dead68

File tree

3 files changed

+130
-16
lines changed

3 files changed

+130
-16
lines changed

resources/js/components/app-sidebar.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { type NavItem } from '@/types';
1515
import { Link } from '@inertiajs/react';
1616
import { BookOpen, Folder, LayoutGrid } from 'lucide-react';
1717
import AppLogo from './app-logo';
18+
import { useState } from 'react';
1819

1920
const mainNavItems: NavItem[] = [
2021
{
@@ -38,6 +39,13 @@ const footerNavItems: NavItem[] = [
3839
];
3940

4041
export function AppSidebar() {
42+
43+
const [openSubmenu, setOpenSubmenu] = useState<{
44+
id: string;
45+
index: number;
46+
} | null>(null);
47+
48+
4149
return (
4250
<Sidebar collapsible="icon" variant="inset">
4351
<SidebarHeader>
@@ -53,7 +61,7 @@ export function AppSidebar() {
5361
</SidebarHeader>
5462

5563
<SidebarContent>
56-
<NavMain items={mainNavItems} />
64+
<NavMain items={mainNavItems} openSubmenu={openSubmenu} setOpenSubmenu={setOpenSubmenu}/>
5765
</SidebarContent>
5866

5967
<SidebarFooter>

resources/js/components/nav-main.tsx

Lines changed: 118 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,31 +4,136 @@ import {
44
SidebarMenu,
55
SidebarMenuButton,
66
SidebarMenuItem,
7+
useSidebar,
78
} from '@/components/ui/sidebar';
89
import { resolveUrl } from '@/lib/utils';
910
import { type NavItem } from '@/types';
1011
import { Link, usePage } from '@inertiajs/react';
12+
import { ChevronDownIcon } from 'lucide-react';
13+
import { useEffect, useId, useRef, useState } from 'react';
1114

12-
export function NavMain({ items = [] }: { items: NavItem[] }) {
15+
export function NavMain({ lable = "Platform", items = [], openSubmenu, setOpenSubmenu }: {
16+
lable?: string;
17+
items: NavItem[];
18+
openSubmenu: { id: string; index: number } | null;
19+
setOpenSubmenu: React.Dispatch<React.SetStateAction<{ id: string; index: number } | null>>;
20+
}) {
1321
const page = usePage();
22+
23+
const id = useRef<string>(useId());
24+
25+
const { isMobile, state, openMobile } = useSidebar();
26+
27+
const subMenuRefs = useRef<Record<string, HTMLDivElement | null>>({});
28+
29+
const [subMenuHeight, setSubMenuHeight] = useState<Record<string, number>>(
30+
{}
31+
);
32+
33+
const handleSubmenuToggle = (index: number, id: string) => {
34+
setOpenSubmenu((prevOpenSubmenu) => {
35+
if (
36+
prevOpenSubmenu &&
37+
prevOpenSubmenu.id === id &&
38+
prevOpenSubmenu.index === index
39+
) {
40+
return null;
41+
}
42+
return { id, index };
43+
});
44+
}
45+
46+
useEffect(() => {
47+
if (openSubmenu !== null) {
48+
const key = `${openSubmenu.id}-${openSubmenu.index}`;
49+
if (subMenuRefs.current[key]) {
50+
setSubMenuHeight((prevHeights) => ({
51+
...prevHeights,
52+
[key]: subMenuRefs.current[key]?.scrollHeight || 0,
53+
}));
54+
}
55+
}
56+
}, [openSubmenu]);
57+
58+
const activeGroup = (navItem: NavItem) => {
59+
let foundItem = items.find((item) => {
60+
if (navItem.href && page.url.startsWith(resolveUrl(navItem.href))) {
61+
return item;
62+
}
63+
});
64+
65+
if (foundItem) return true;
66+
else return false;
67+
}
68+
1469
return (
1570
<SidebarGroup className="px-2 py-0">
16-
<SidebarGroupLabel>Platform</SidebarGroupLabel>
71+
<SidebarGroupLabel>{lable}</SidebarGroupLabel>
1772
<SidebarMenu>
18-
{items.map((item) => (
73+
{items.map((item, index) => (
1974
<SidebarMenuItem key={item.title}>
20-
<SidebarMenuButton
21-
asChild
22-
isActive={page.url.startsWith(
23-
resolveUrl(item.href),
24-
)}
25-
tooltip={{ children: item.title }}
26-
>
27-
<Link href={item.href} prefetch>
75+
{item.subItems ? (<>
76+
<SidebarMenuButton
77+
isActive={activeGroup(item)}
78+
className="cursor-pointer"
79+
tooltip={{ children: item.title }}
80+
onClick={() => handleSubmenuToggle(index, id.current)}
81+
>
2882
{item.icon && <item.icon />}
2983
<span>{item.title}</span>
30-
</Link>
31-
</SidebarMenuButton>
84+
<ChevronDownIcon
85+
className={`ms-auto h-4 w-4 shrink-0 opacity-50 transition-all duration-300 ${openSubmenu?.id === id.current && openSubmenu?.index === index ? "rotate-180" : ""}`}
86+
/>
87+
</SidebarMenuButton>
88+
89+
{(state === "expanded" || (isMobile && openMobile)) && (
90+
<div
91+
ref={(el) => {
92+
subMenuRefs.current[`${id.current}-${index}`] = el;
93+
}}
94+
className="overflow-hidden transition-all duration-300"
95+
style={{
96+
height:
97+
openSubmenu?.id === id.current && openSubmenu?.index === index
98+
? `${subMenuHeight[`${id.current}-${index}`]}px`
99+
: "0px",
100+
}}
101+
>
102+
<SidebarMenu className="mt-2 ms-6 w-auto">
103+
{item.subItems.map((subItem) => (
104+
<SidebarMenuItem key={subItem.title}>
105+
<SidebarMenuButton
106+
107+
asChild
108+
isActive={subItem.href ? page.url.startsWith(
109+
resolveUrl(subItem.href),
110+
): false}
111+
tooltip={{ children: subItem.title }}
112+
>
113+
<Link href={subItem.href!} prefetch>
114+
{subItem.icon && <subItem.icon />}
115+
<span>{subItem.title}</span>
116+
</Link>
117+
</SidebarMenuButton>
118+
</SidebarMenuItem>
119+
))}
120+
</SidebarMenu>
121+
</div>
122+
)}
123+
</>) : (
124+
<SidebarMenuButton
125+
asChild
126+
isActive={item.href ? page.url.startsWith(
127+
resolveUrl(item.href),
128+
): false}
129+
tooltip={{ children: item.title }}
130+
>
131+
<Link href={item.href} prefetch>
132+
{item.icon && <item.icon />}
133+
<span>{item.title}</span>
134+
</Link>
135+
</SidebarMenuButton>
136+
)}
32137
</SidebarMenuItem>
33138
))}
34139
</SidebarMenu>

resources/js/types/index.d.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@ export interface NavGroup {
1717

1818
export interface NavItem {
1919
title: string;
20-
href: NonNullable<InertiaLinkProps['href']>;
21-
icon?: LucideIcon | null;
20+
href?: NonNullable<InertiaLinkProps['href']>;
21+
icon?: LucideIcon | any;
2222
isActive?: boolean;
23+
subItems?: NavItem[];
2324
}
2425

2526
export interface SharedData {

0 commit comments

Comments
 (0)