MobileMenuContent.tsx203 lines · main
| 1 | 'use client' |
| 2 | |
| 3 | import { useFlag, useParams } from 'common' |
| 4 | import { Home } from 'icons' |
| 5 | import { ChevronLeft } from 'lucide-react' |
| 6 | import { useRouter } from 'next/router' |
| 7 | import React, { useMemo } from 'react' |
| 8 | import { Button, cn, Separator, SidebarGroup, SidebarMenu } from 'ui' |
| 9 | import { GenericSkeletonLoader } from 'ui-patterns' |
| 10 | |
| 11 | import { resolveSectionDisplay } from './MobileMenuContent.utils' |
| 12 | import { getProductMenuComponent } from './mobileProductMenuRegistry' |
| 13 | import { TopLevelRouteItem } from './TopLevelRouteItem' |
| 14 | import { routeHasSubmenu, useMobileMenuNavigation } from './useMobileMenuNavigation' |
| 15 | import { useUnifiedLogsPreview } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext' |
| 16 | import { ICON_SIZE, ICON_STROKE_WIDTH } from '@/components/interfaces/Sidebar' |
| 17 | import { |
| 18 | generateOtherRoutes, |
| 19 | generateProductRoutes, |
| 20 | generateSettingsRoutes, |
| 21 | generateToolRoutes, |
| 22 | } from '@/components/layouts/Navigation/NavigationBar/NavigationBar.utils' |
| 23 | import type { Route } from '@/components/ui/ui.types' |
| 24 | import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled' |
| 25 | import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' |
| 26 | import { getPathnameWithoutQuery, getPathSegment } from '@/lib/pathname.utils' |
| 27 | |
| 28 | export interface MobileMenuContentProps { |
| 29 | currentProductMenu: React.ReactNode |
| 30 | currentProduct: string |
| 31 | currentSectionKey: string | null |
| 32 | onCloseSheet?: () => void |
| 33 | } |
| 34 | |
| 35 | export 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 | } |