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