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