nav-group.tsx 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. import { type ReactNode } from 'react'
  2. import { Link, useLocation } from '@tanstack/react-router'
  3. import { ChevronRight } from 'lucide-react'
  4. import {
  5. Collapsible,
  6. CollapsibleContent,
  7. CollapsibleTrigger,
  8. } from '@/components/ui/collapsible'
  9. import {
  10. SidebarGroup,
  11. SidebarGroupLabel,
  12. SidebarMenu,
  13. SidebarMenuButton,
  14. SidebarMenuItem,
  15. SidebarMenuSub,
  16. SidebarMenuSubButton,
  17. SidebarMenuSubItem,
  18. useSidebar,
  19. } from '@/components/ui/sidebar'
  20. import { Badge } from '../ui/badge'
  21. import {
  22. DropdownMenu,
  23. DropdownMenuContent,
  24. DropdownMenuItem,
  25. DropdownMenuLabel,
  26. DropdownMenuSeparator,
  27. DropdownMenuTrigger,
  28. } from '../ui/dropdown-menu'
  29. import { type NavCollapsible, type NavItem, type NavLink, type NavGroup } from './types'
  30. export function NavGroup({ title, items }: NavGroup) {
  31. const { state, isMobile } = useSidebar()
  32. const href = useLocation({ select: (location) => location.href })
  33. return (
  34. <SidebarGroup>
  35. <SidebarGroupLabel>{title}</SidebarGroupLabel>
  36. <SidebarMenu>
  37. {items.map((item) => {
  38. const key = `${item.title}-${item.url}`
  39. if (!item.items)
  40. return <SidebarMenuLink key={key} item={item} href={href} />
  41. if (state === 'collapsed' && !isMobile)
  42. return (
  43. <SidebarMenuCollapsedDropdown key={key} item={item} href={href} />
  44. )
  45. return <SidebarMenuCollapsible key={key} item={item} href={href} />
  46. })}
  47. </SidebarMenu>
  48. </SidebarGroup>
  49. )
  50. }
  51. function NavBadge({ children }: { children: ReactNode }) {
  52. return <Badge className='rounded-full px-1 py-0 text-xs'>{children}</Badge>
  53. }
  54. function SidebarMenuLink({ item, href }: { item: NavLink; href: string }) {
  55. const { setOpenMobile } = useSidebar()
  56. return (
  57. <SidebarMenuItem>
  58. <SidebarMenuButton
  59. asChild
  60. isActive={checkIsActive(href, item)}
  61. tooltip={item.title}
  62. >
  63. <Link to={item.url} onClick={() => setOpenMobile(false)}>
  64. {item.icon && <item.icon />}
  65. <span>{item.title}</span>
  66. {item.badge && <NavBadge>{item.badge}</NavBadge>}
  67. </Link>
  68. </SidebarMenuButton>
  69. </SidebarMenuItem>
  70. )
  71. }
  72. function SidebarMenuCollapsible({
  73. item,
  74. href,
  75. }: {
  76. item: NavCollapsible
  77. href: string
  78. }) {
  79. const { setOpenMobile } = useSidebar()
  80. return (
  81. <Collapsible
  82. asChild
  83. defaultOpen={checkIsActive(href, item, true)}
  84. className='group/collapsible'
  85. >
  86. <SidebarMenuItem>
  87. <CollapsibleTrigger asChild>
  88. <SidebarMenuButton tooltip={item.title}>
  89. {item.icon && <item.icon />}
  90. <span>{item.title}</span>
  91. {item.badge && <NavBadge>{item.badge}</NavBadge>}
  92. <ChevronRight className='ms-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90' />
  93. </SidebarMenuButton>
  94. </CollapsibleTrigger>
  95. <CollapsibleContent className='CollapsibleContent'>
  96. <SidebarMenuSub>
  97. {item.items.map((subItem) => (
  98. <SidebarMenuSubItem key={subItem.title}>
  99. <SidebarMenuSubButton
  100. asChild
  101. isActive={checkIsActive(href, subItem)}
  102. >
  103. <Link to={subItem.url} onClick={() => setOpenMobile(false)}>
  104. {subItem.icon && <subItem.icon />}
  105. <span>{subItem.title}</span>
  106. {subItem.badge && <NavBadge>{subItem.badge}</NavBadge>}
  107. </Link>
  108. </SidebarMenuSubButton>
  109. </SidebarMenuSubItem>
  110. ))}
  111. </SidebarMenuSub>
  112. </CollapsibleContent>
  113. </SidebarMenuItem>
  114. </Collapsible>
  115. )
  116. }
  117. function SidebarMenuCollapsedDropdown({
  118. item,
  119. href,
  120. }: {
  121. item: NavCollapsible
  122. href: string
  123. }) {
  124. return (
  125. <SidebarMenuItem>
  126. <DropdownMenu>
  127. <DropdownMenuTrigger asChild>
  128. <SidebarMenuButton
  129. tooltip={item.title}
  130. isActive={checkIsActive(href, item)}
  131. >
  132. {item.icon && <item.icon />}
  133. <span>{item.title}</span>
  134. {item.badge && <NavBadge>{item.badge}</NavBadge>}
  135. <ChevronRight className='ms-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90' />
  136. </SidebarMenuButton>
  137. </DropdownMenuTrigger>
  138. <DropdownMenuContent side='right' align='start' sideOffset={4}>
  139. <DropdownMenuLabel>
  140. {item.title} {item.badge ? `(${item.badge})` : ''}
  141. </DropdownMenuLabel>
  142. <DropdownMenuSeparator />
  143. {item.items.map((sub) => (
  144. <DropdownMenuItem key={`${sub.title}-${sub.url}`} asChild>
  145. <Link
  146. to={sub.url}
  147. className={`${checkIsActive(href, sub) ? 'bg-secondary' : ''}`}
  148. >
  149. {sub.icon && <sub.icon />}
  150. <span className='max-w-52 text-wrap'>{sub.title}</span>
  151. {sub.badge && (
  152. <span className='ms-auto text-xs'>{sub.badge}</span>
  153. )}
  154. </Link>
  155. </DropdownMenuItem>
  156. ))}
  157. </DropdownMenuContent>
  158. </DropdownMenu>
  159. </SidebarMenuItem>
  160. )
  161. }
  162. function checkIsActive(href: string, item: NavItem, mainNav = false) {
  163. return (
  164. href === item.url || // /endpint?search=param
  165. href.split('?')[0] === item.url || // endpoint
  166. !!item?.items?.filter((i) => i.url === href).length || // if child nav is active
  167. (mainNav &&
  168. href.split('/')[1] !== '' &&
  169. href.split('/')[1] === item?.url?.split('/')[1])
  170. )
  171. }