Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
189 changes: 130 additions & 59 deletions src/components/layout/nav-group.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,75 +18,35 @@ import {
useSidebar,
} from '@/components/ui/sidebar'
import { Badge } from '../ui/badge'
import { NavItem, type NavGroup } from './types'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '../ui/dropdown-menu'
import { NavCollapsible, NavItem, NavLink, type NavGroup } from './types'

export function NavGroup({ title, items }: NavGroup) {
const { setOpenMobile } = useSidebar()
const { state } = useSidebar()
const href = useLocation({ select: (location) => location.href })
return (
<SidebarGroup>
<SidebarGroupLabel>{title}</SidebarGroupLabel>
<SidebarMenu>
{items.map((item) => {
if (!item.items) {
const key = `${item.title}-${item.url}`

if (!item.items)
return <SidebarMenuLink key={key} item={item} href={href} />

if (state === 'collapsed')
return (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton
asChild
isActive={checkIsActive(href, item)}
tooltip={item.title}
>
<Link to={item.url} onClick={() => setOpenMobile(false)}>
{item.icon && <item.icon />}
<span>{item.title}</span>
{item.badge && <NavBadge>{item.badge}</NavBadge>}
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuCollapsedDropdown key={key} item={item} href={href} />
)
}
return (
<Collapsible
key={item.title}
asChild
defaultOpen={checkIsActive(href, item, true)}
className='group/collapsible'
>
<SidebarMenuItem>
<CollapsibleTrigger asChild>
<SidebarMenuButton tooltip={item.title}>
{item.icon && <item.icon />}
<span>{item.title}</span>
{item.badge && <NavBadge>{item.badge}</NavBadge>}
<ChevronRight className='ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90' />
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent className='CollapsibleContent'>
<SidebarMenuSub>
{item.items.map((subItem) => (
<SidebarMenuSubItem key={subItem.title}>
<SidebarMenuSubButton
asChild
isActive={checkIsActive(href, subItem)}
>
<Link
to={subItem.url}
onClick={() => setOpenMobile(false)}
>
{subItem.icon && <subItem.icon />}
<span>{subItem.title}</span>
{subItem.badge && (
<NavBadge>{subItem.badge}</NavBadge>
)}
</Link>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
))}
</SidebarMenuSub>
</CollapsibleContent>
</SidebarMenuItem>
</Collapsible>
)

return <SidebarMenuCollapsible key={key} item={item} href={href} />
})}
</SidebarMenu>
</SidebarGroup>
Expand All @@ -97,6 +57,117 @@ const NavBadge = ({ children }: { children: ReactNode }) => (
<Badge className='text-xs rounded-full px-1 py-0'>{children}</Badge>
)

const SidebarMenuLink = ({ item, href }: { item: NavLink; href: string }) => {
const { setOpenMobile } = useSidebar()
return (
<SidebarMenuItem>
<SidebarMenuButton
asChild
isActive={checkIsActive(href, item)}
tooltip={item.title}
>
<Link to={item.url} onClick={() => setOpenMobile(false)}>
{item.icon && <item.icon />}
<span>{item.title}</span>
{item.badge && <NavBadge>{item.badge}</NavBadge>}
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
)
}

const SidebarMenuCollapsible = ({
item,
href,
}: {
item: NavCollapsible
href: string
}) => {
const { setOpenMobile } = useSidebar()
return (
<Collapsible
asChild
defaultOpen={checkIsActive(href, item, true)}
className='group/collapsible'
>
<SidebarMenuItem>
<CollapsibleTrigger asChild>
<SidebarMenuButton tooltip={item.title}>
{item.icon && <item.icon />}
<span>{item.title}</span>
{item.badge && <NavBadge>{item.badge}</NavBadge>}
<ChevronRight className='ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90' />
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent className='CollapsibleContent'>
<SidebarMenuSub>
{item.items.map((subItem) => (
<SidebarMenuSubItem key={subItem.title}>
<SidebarMenuSubButton
asChild
isActive={checkIsActive(href, subItem)}
>
<Link to={subItem.url} onClick={() => setOpenMobile(false)}>
{subItem.icon && <subItem.icon />}
<span>{subItem.title}</span>
{subItem.badge && <NavBadge>{subItem.badge}</NavBadge>}
</Link>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
))}
</SidebarMenuSub>
</CollapsibleContent>
</SidebarMenuItem>
</Collapsible>
)
}

const SidebarMenuCollapsedDropdown = ({
item,
href,
}: {
item: NavCollapsible
href: string
}) => {
return (
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuButton
tooltip={item.title}
isActive={checkIsActive(href, item)}
>
{item.icon && <item.icon />}
<span>{item.title}</span>
{item.badge && <NavBadge>{item.badge}</NavBadge>}
<ChevronRight className='ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90' />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent side='right' align='start' sideOffset={4}>
<DropdownMenuLabel>
{item.title} {item.badge ? `(${item.badge})` : ''}
</DropdownMenuLabel>
<DropdownMenuSeparator />
{item.items.map((sub) => (
<DropdownMenuItem key={`${sub.title}-${sub.url}`} asChild>
<Link
to={sub.url}
className={`${checkIsActive(href, sub) ? 'bg-secondary' : ''}`}
>
{sub.icon && <sub.icon />}
<span className='max-w-52 text-wrap'>{sub.title}</span>
{sub.badge && (
<span className='ml-auto text-xs'>{sub.badge}</span>
)}
</Link>
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
)
}

function checkIsActive(href: string, item: NavItem, mainNav = false) {
return (
href === item.url || // /endpint?search=param
Expand Down
22 changes: 12 additions & 10 deletions src/components/layout/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,17 @@ interface BaseNavItem {
icon?: React.ElementType
}

export type NavItem =
| (BaseNavItem & {
items: (BaseNavItem & { url: LinkProps['to'] })[]
url?: never
})
| (BaseNavItem & {
url: LinkProps['to']
items?: never
})
type NavLink = BaseNavItem & {
url: LinkProps['to']
items?: never
}

type NavCollapsible = BaseNavItem & {
items: (BaseNavItem & { url: LinkProps['to'] })[]
url?: never
}

type NavItem = NavCollapsible | NavLink

interface NavGroup {
title: string
Expand All @@ -39,4 +41,4 @@ interface SidebarData {
navGroups: NavGroup[]
}

export type { SidebarData, NavGroup }
export type { SidebarData, NavGroup, NavItem, NavCollapsible, NavLink }
Loading