1import { useRouter } from 'next/compat/router'; 2import React, { FC, useMemo } from 'react'; 3 4import { ApiVersionSelect } from './ApiVersionSelect'; 5import { GroupList } from './GroupList'; 6import { PageLink } from './PageLink'; 7import { SectionList } from './SectionList'; 8import { NavigationNode, NavigationRenderProps, NavigationType } from './types'; 9 10import { LayoutScroll, usePersistScroll } from '~/ui/components/Layout'; 11 12export type NavigationProps = { 13 /** The tree of navigation nodes to render in the sidebar */ 14 routes: NavigationNode[]; 15}; 16 17export function Navigation({ routes }: NavigationProps) { 18 const router = useRouter(); 19 const activeRoutes = useMemo(() => findActiveRoute(routes, router?.pathname), [router?.pathname]); 20 const persistScroll = usePersistScroll('navigation'); 21 22 return ( 23 <nav className="w-[280px] h-full bg-subtle dark:bg-default"> 24 <LayoutScroll {...persistScroll}> 25 <ApiVersionSelect /> 26 {routes.map(route => navigationRenderer(route, activeRoutes))} 27 </LayoutScroll> 28 </nav> 29 ); 30} 31 32const renderers: Record<NavigationType, FC<React.PropsWithChildren<NavigationRenderProps>>> = { 33 section: SectionList, 34 group: GroupList, 35 page: PageLink, 36}; 37 38function navigationRenderer( 39 route: NavigationNode, 40 activeRoutes: Record<NavigationType, NavigationNode | null> 41) { 42 if (route.hidden) return null; 43 const Component = renderers[route.type]; 44 const routeKey = `${route.type}-${route.name}`; 45 const isActive = activeRoutes[route.type] === route; 46 const hasChildren = route.type !== 'page' && route.children.length; 47 return ( 48 <Component key={routeKey} route={route} isActive={isActive}> 49 {hasChildren && route.children.map(nested => navigationRenderer(nested, activeRoutes))} 50 </Component> 51 ); 52} 53 54/** 55 * Find the active routes by pathname, and do it once. 56 * This will iterate the tree and find the nodes which are "active". 57 * - Page -> if the pathname matches the page's href property 58 * - Group -> if the group contains an active page 59 * - Section -> if the section contains an active group or page 60 */ 61export function findActiveRoute(routes: NavigationNode[], pathname?: string) { 62 const activeRoutes: Record<NavigationType, NavigationNode | null> = { 63 page: null, 64 group: null, 65 section: null, 66 }; 67 68 if (!pathname) { 69 return activeRoutes; 70 } 71 72 for (const route of routes) { 73 // Try to exit early on hidden routes 74 if (route.hidden) continue; 75 76 switch (route.type) { 77 case 'page': 78 if (route.href === pathname) { 79 activeRoutes.page = route; 80 break; 81 } 82 break; 83 84 case 'group': 85 { 86 const nestedActiveRoutes = findActiveRoute(route.children, pathname); 87 if (nestedActiveRoutes?.page) { 88 activeRoutes.page = nestedActiveRoutes.page; 89 activeRoutes.group = route; 90 break; 91 } 92 } 93 break; 94 95 case 'section': 96 { 97 const nestedActiveRoutes = findActiveRoute(route.children, pathname); 98 if (nestedActiveRoutes?.group || nestedActiveRoutes?.page) { 99 activeRoutes.page = nestedActiveRoutes.page; 100 activeRoutes.group = nestedActiveRoutes.group; 101 activeRoutes.section = route; 102 break; 103 } 104 } 105 break; 106 } 107 } 108 109 return activeRoutes; 110} 111