MobileMenuContent.tsx 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. 'use client'
  2. import { useFlag, useParams } from 'common'
  3. import { Home } from 'icons'
  4. import { ChevronLeft } from 'lucide-react'
  5. import { useRouter } from 'next/router'
  6. import React, { useMemo } from 'react'
  7. import { Button, cn, Separator, SidebarGroup, SidebarMenu } from 'ui'
  8. import { GenericSkeletonLoader } from 'ui-patterns'
  9. import { resolveSectionDisplay } from './MobileMenuContent.utils'
  10. import { getProductMenuComponent } from './mobileProductMenuRegistry'
  11. import { TopLevelRouteItem } from './TopLevelRouteItem'
  12. import { routeHasSubmenu, useMobileMenuNavigation } from './useMobileMenuNavigation'
  13. import { useUnifiedLogsPreview } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext'
  14. import { ICON_SIZE, ICON_STROKE_WIDTH } from '@/components/interfaces/Sidebar'
  15. import {
  16. generateOtherRoutes,
  17. generateProductRoutes,
  18. generateSettingsRoutes,
  19. generateToolRoutes,
  20. } from '@/components/layouts/Navigation/NavigationBar/NavigationBar.utils'
  21. import type { Route } from '@/components/ui/ui.types'
  22. import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
  23. import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
  24. import { getPathnameWithoutQuery, getPathSegment } from '@/lib/pathname.utils'
  25. export interface MobileMenuContentProps {
  26. currentProductMenu: React.ReactNode
  27. currentProduct: string
  28. currentSectionKey: string | null
  29. onCloseSheet?: () => void
  30. }
  31. export function MobileMenuContent({
  32. currentProductMenu,
  33. currentProduct,
  34. currentSectionKey,
  35. onCloseSheet,
  36. }: MobileMenuContentProps) {
  37. const router = useRouter()
  38. const { ref } = useParams()
  39. const { data: project } = useSelectedProjectQuery()
  40. const pathname = getPathnameWithoutQuery(router.asPath, router.pathname)
  41. const activeRoute = getPathSegment(pathname, 3)
  42. const { viewLevel, selectedSectionKey, handleTopLevelClick, handleBackToTop } =
  43. useMobileMenuNavigation({
  44. currentSectionKey,
  45. hasCurrentProductMenu: !!currentProductMenu,
  46. onCloseSheet,
  47. })
  48. const {
  49. projectAuthAll: authEnabled,
  50. projectEdgeFunctionAll: edgeFunctionsEnabled,
  51. projectStorageAll: storageEnabled,
  52. realtimeAll: realtimeEnabled,
  53. } = useIsFeatureEnabled([
  54. 'project_auth:all',
  55. 'project_edge_function:all',
  56. 'project_storage:all',
  57. 'realtime:all',
  58. ])
  59. const authOverviewPageEnabled = useFlag('authOverviewPage')
  60. const showReports = useIsFeatureEnabled('reports:all')
  61. const { isEnabled: isUnifiedLogsEnabled } = useUnifiedLogsPreview()
  62. const toolRoutes = useMemo(() => generateToolRoutes(ref, project), [ref, project])
  63. const productRoutes = useMemo(
  64. () =>
  65. generateProductRoutes(ref, project, {
  66. auth: authEnabled,
  67. edgeFunctions: edgeFunctionsEnabled,
  68. storage: storageEnabled,
  69. realtime: realtimeEnabled,
  70. authOverviewPage: authOverviewPageEnabled,
  71. }),
  72. [
  73. ref,
  74. project,
  75. authEnabled,
  76. edgeFunctionsEnabled,
  77. storageEnabled,
  78. realtimeEnabled,
  79. authOverviewPageEnabled,
  80. ]
  81. )
  82. const otherRoutes = useMemo(
  83. () =>
  84. generateOtherRoutes(ref, project, {
  85. unifiedLogs: isUnifiedLogsEnabled,
  86. showReports,
  87. }),
  88. [ref, project, isUnifiedLogsEnabled, showReports]
  89. )
  90. const settingsRoutes = useMemo(() => generateSettingsRoutes(ref), [ref])
  91. const homeRoute: Route = useMemo(
  92. () => ({
  93. key: 'HOME',
  94. label: 'Project Overview',
  95. icon: <Home size={ICON_SIZE} strokeWidth={ICON_STROKE_WIDTH} />,
  96. link: ref ? `/project/${ref}` : undefined,
  97. }),
  98. [ref]
  99. )
  100. const allTopLevelRoutes = useMemo(
  101. () => [homeRoute, ...toolRoutes, ...productRoutes, ...otherRoutes, ...settingsRoutes],
  102. [homeRoute, toolRoutes, productRoutes, otherRoutes, settingsRoutes]
  103. )
  104. const { sectionKey: sectionKeyToShow, sectionLabel } = resolveSectionDisplay({
  105. viewLevel,
  106. selectedSectionKey,
  107. currentSectionKey,
  108. currentProduct,
  109. routes: allTopLevelRoutes,
  110. })
  111. const SectionMenuContent = sectionKeyToShow ? getProductMenuComponent(sectionKeyToShow) : null
  112. const pageSegment = getPathSegment(pathname, 4)
  113. const renderRoute = (route: Route, isActive: boolean) => (
  114. <TopLevelRouteItem
  115. key={route.key}
  116. route={route}
  117. isActive={isActive}
  118. hasSubmenu={routeHasSubmenu(route)}
  119. onTopLevelClick={handleTopLevelClick}
  120. onCloseSheet={onCloseSheet}
  121. />
  122. )
  123. return (
  124. <div className="flex flex-col h-full bg-background">
  125. {viewLevel === 'section' && sectionLabel && (
  126. <div
  127. className={cn(
  128. 'shrink-0 flex items-center gap-2 border-b border-default px-3 min-h-(--header-height)'
  129. )}
  130. >
  131. <Button
  132. type="text"
  133. className="p-1! justify-start"
  134. icon={<ChevronLeft size={20} />}
  135. onClick={handleBackToTop}
  136. aria-label="Back to menu"
  137. block
  138. >
  139. <span className="font-medium truncate text-sm">{sectionLabel}</span>
  140. </Button>
  141. </div>
  142. )}
  143. <div className="flex-1 overflow-y-auto pb-8 text-sidebar-foreground">
  144. {viewLevel === 'top' && (
  145. <nav className="flex flex-col gap-2 p-1" aria-label="Project menu">
  146. <SidebarMenu>
  147. <SidebarGroup className="gap-0.5">
  148. {[homeRoute, ...toolRoutes].map((route) =>
  149. renderRoute(
  150. route,
  151. activeRoute === route.key || (route.key === 'HOME' && activeRoute === undefined)
  152. )
  153. )}
  154. </SidebarGroup>
  155. <Separator className="mx-2 w-auto bg-sidebar-border" />
  156. <SidebarGroup className="gap-0.5">
  157. {productRoutes.map((route) => renderRoute(route, activeRoute === route.key))}
  158. </SidebarGroup>
  159. <Separator className="mx-2 w-auto bg-sidebar-border" />
  160. <SidebarGroup className="gap-0.5">
  161. {otherRoutes.map((route) => renderRoute(route, activeRoute === route.key))}
  162. </SidebarGroup>
  163. <Separator className="mx-2 w-auto bg-sidebar-border" />
  164. <SidebarGroup className="gap-0.5">
  165. {settingsRoutes.map((route) => renderRoute(route, activeRoute === route.key))}
  166. </SidebarGroup>
  167. </SidebarMenu>
  168. </nav>
  169. )}
  170. {viewLevel === 'section' && sectionKeyToShow && (
  171. <div className="p-1">
  172. {sectionKeyToShow === currentSectionKey && currentProductMenu ? (
  173. currentProductMenu
  174. ) : SectionMenuContent ? (
  175. <React.Suspense fallback={<GenericSkeletonLoader className="p-4" />}>
  176. {sectionKeyToShow === 'advisors' ? (
  177. <SectionMenuContent
  178. {...({ page: pageSegment } as React.ComponentProps<typeof SectionMenuContent>)}
  179. />
  180. ) : (
  181. <SectionMenuContent />
  182. )}
  183. </React.Suspense>
  184. ) : null}
  185. </div>
  186. )}
  187. </div>
  188. </div>
  189. )
  190. }