done with distributive services

- duplicated side bar still exists
This commit is contained in:
Zheyuan Wu
2025-10-25 00:49:35 -05:00
parent 3aceb5ba0b
commit f124f7a744
6 changed files with 401 additions and 46 deletions

View File

@@ -2,6 +2,7 @@
// sample code from https://docsearch.algolia.com/docs/docsearch // sample code from https://docsearch.algolia.com/docs/docsearch
import { DocSearch } from '@docsearch/react'; import { DocSearch } from '@docsearch/react';
import { useThemeConfig } from 'nextra-theme-docs'
import '@docsearch/css'; import '@docsearch/css';
@@ -11,6 +12,7 @@ function AlgoliaSearch() {
appId={process.env.NEXT_SEARCH_ALGOLIA_APP_ID || 'NKGLZZZUBC'} appId={process.env.NEXT_SEARCH_ALGOLIA_APP_ID || 'NKGLZZZUBC'}
indexName={process.env.NEXT_SEARCH_ALGOLIA_INDEX_NAME || 'notenextra_trance_0'} indexName={process.env.NEXT_SEARCH_ALGOLIA_INDEX_NAME || 'notenextra_trance_0'}
apiKey={process.env.NEXT_SEARCH_ALGOLIA_API_KEY || '727b389a61e862e590dfab9ce9df31a2'} apiKey={process.env.NEXT_SEARCH_ALGOLIA_API_KEY || '727b389a61e862e590dfab9ce9df31a2'}
theme={useThemeConfig().darkMode ? 'dark' : 'light'}
/> />
); );
} }

View File

@@ -0,0 +1,188 @@
'use client'
import {
MenuItem as _MenuItem,
Menu,
MenuButton,
MenuItems
} from '@headlessui/react'
import cn from 'clsx'
import { Anchor, Button } from 'nextra/components'
import { useFSRoute } from 'nextra/hooks'
import { ArrowRightIcon, MenuIcon } from 'nextra/icons'
import type { MenuItem } from 'nextra/normalize-pages'
import type { FC, ReactNode } from 'react'
import { setMenu, useConfig, useMenu, useThemeConfig } from 'nextra-theme-docs'
import { usePathname } from 'next/navigation'
import { normalizePages } from 'nextra/normalize-pages'
import { PageMapItem } from 'nextra'
const classes = {
link: cn(
'x:text-sm x:contrast-more:text-gray-700 x:contrast-more:dark:text-gray-100 x:whitespace-nowrap',
'x:text-gray-600 x:hover:text-black x:dark:text-gray-400 x:dark:hover:text-gray-200',
'x:ring-inset x:transition-colors'
)
}
const NavbarMenu: FC<{
menu: MenuItem
children: ReactNode
}> = ({ menu, children }) => {
const routes = Object.fromEntries(
(menu.children || []).map(route => [route.name, route])
)
return (
<Menu>
<MenuButton
className={({ focus }) =>
cn(
classes.link,
'x:items-center x:flex x:gap-1.5 x:cursor-pointer',
focus && 'x:nextra-focus'
)
}
>
{children}
<ArrowRightIcon
height="14"
className="x:*:origin-center x:*:transition-transform x:*:rotate-90"
/>
</MenuButton>
<MenuItems
transition
className={cn(
'x:focus-visible:nextra-focus',
'nextra-scrollbar x:motion-reduce:transition-none',
// From https://headlessui.com/react/menu#adding-transitions
'x:origin-top x:transition x:duration-200 x:ease-out x:data-closed:scale-95 x:data-closed:opacity-0',
'x:border x:border-black/5 x:dark:border-white/20',
'x:z-30 x:rounded-md x:py-1 x:text-sm x:shadow-lg',
'x:backdrop-blur-md x:bg-nextra-bg/70',
// headlessui adds max-height as style, use !important to override
'x:max-h-[min(calc(100vh-5rem),256px)]!'
)}
anchor={{ to: 'bottom', gap: 10, padding: 16 }}
>
{Object.entries(
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- fixme
(menu.items as Record<string, { title: string; href?: string }>) || {}
).map(([key, item]) => (
<_MenuItem
key={key}
as={Anchor}
href={item.href || routes[key]?.route}
className={({ focus }) =>
cn(
'x:block x:py-1.5 x:transition-colors x:ps-3 x:pe-9',
focus
? 'x:text-gray-900 x:dark:text-gray-100'
: 'x:text-gray-600 x:dark:text-gray-400'
)
}
>
{item.title}
</_MenuItem>
))}
</MenuItems>
</Menu>
)
}
const isMenu = (page: any): page is MenuItem => page.type === 'menu'
export const ClientNavbar: FC<{
pageMap: PageMapItem[]
children: ReactNode
className?: string
}> = ({ pageMap, children, className }) => {
const { topLevelNavbarItems } = normalizePages({
list: pageMap,
route: usePathname()
})
// filter out titles for elements in topLevelNavbarItems with non empty route
const existingCourseNames = new Set(
topLevelNavbarItems.filter(
item => !('href' in item)
).map(item => item.title)
)
// filter out elements in topLevelNavbarItems with url but have title in existingCourseNames
const filteredTopLevelNavbarItems = topLevelNavbarItems.filter(item => !('href' in item && existingCourseNames.has(item.title)))
// or even better, remove all old doc page generated links
// this don't make sense because it will destroy the navbar structure highlight for children pages
// const filteredTopLevelNavbarItems = topLevelNavbarItems.filter(item => !('firstChildRoute' in item))
// const items = topLevelNavbarItems
// use filteredTopLevelNavbarItems to generate items
const items = filteredTopLevelNavbarItems
const themeConfig = useThemeConfig()
const pathname = useFSRoute()
const menu = useMenu()
return (
<>
<div
className={cn(
'x:flex x:gap-4 x:overflow-x-auto nextra-scrollbar x:py-1.5 x:max-md:hidden',
className
)}
>
{items.map((page, _index, arr) => {
if ('display' in page && page.display === 'hidden') return
if (isMenu(page)) {
return (
<NavbarMenu key={page.name} menu={page}>
{page.title}
</NavbarMenu>
)
}
const href =
// If it's a directory
('frontMatter' in page ? page.route : page.firstChildRoute) ||
page.href ||
page.route
const isCurrentPage =
href === pathname ||
(pathname.startsWith(page.route + '/') &&
arr.every(item => !('href' in item) || item.href !== pathname)) ||
undefined
return (
<Anchor
href={href}
key={page.name}
className={cn(
classes.link,
'x:aria-[current]:font-medium x:aria-[current]:subpixel-antialiased x:aria-[current]:text-current'
)}
aria-current={isCurrentPage}
>
{page.title}
</Anchor>
)
})}
</div>
{themeConfig.search && (
<div className="x:max-md:hidden">{themeConfig.search}</div>
)}
{children}
<Button
aria-label="Menu"
className={({ active }) =>
cn('nextra-hamburger x:md:hidden', active && 'x:bg-gray-400/20')
}
onClick={() => setMenu(prev => !prev)}
>
<MenuIcon height="24" className={cn({ open: menu })} />
</Button>
</>
)
}

170
components/navbar.tsx Normal file
View File

@@ -0,0 +1,170 @@
// customized navbar component, modified from https://github.com/shuding/nextra/blob/c8238813e1ba425cdd72783d57707b0ff3ca52ea/examples/custom-theme/app/_components/navbar.tsx#L9
// Rebuild from source code https://github.com/shuding/nextra/tree/c8238813e1ba425cdd72783d57707b0ff3ca52ea/packages/nextra-theme-docs/src/components/navbar
'use client'
import { usePathname } from 'next/navigation'
import type { PageMapItem } from 'nextra'
import { Anchor } from 'nextra/components'
import { normalizePages } from 'nextra/normalize-pages'
import type { FC, ReactNode } from 'react'
import cn from 'clsx'
// eslint-disable-next-line no-restricted-imports -- since we don't need `newWindow` prop
import NextLink from 'next/link'
import { DiscordIcon, GitHubIcon } from 'nextra/icons'
import { ClientNavbar } from './navbar.client'
// export const Navbar: FC<{ pageMap: PageMapItem[] }> = ({ pageMap }) => {
// const pathname = usePathname()
// const { topLevelNavbarItems } = normalizePages({
// list: pageMap,
// route: pathname
// })
// return (
// <ul
// style={{
// display: 'flex',
// listStyleType: 'none',
// padding: 20,
// gap: 20,
// background: 'lightcoral',
// margin: 0
// }}
// >
// {filteredTopLevelNavbarItems.map(item => {
// const route = item.route || ('href' in item ? item.href! : '')
// return (
// <li key={route}>
// <Anchor href={route} style={{ textDecoration: 'none' }}>
// {item.title}
// </Anchor>
// </li>
// )
// })}
// </ul>
// )
// }
/* TODO: eslint typescript-sort-keys/interface: error */
interface NavbarProps {
/**
* Page map.
*/
pageMap: PageMapItem[]
/**
* Extra content after the last icon.
*/
children?: ReactNode
/**
* Specifies whether the logo should have a link or provides the URL for the logo's link.
* @default true
*/
logoLink?: string | boolean
/**
* Logo of the website.
*/
logo: ReactNode
/**
* URL of the project homepage.
*/
projectLink?: string
/**
* Icon of the project link.
* @default <GitHubIcon />
*/
projectIcon?: ReactNode
/**
* URL of the chat link.
*/
chatLink?: string
/**
* Icon of the chat link.
* @default <DiscordIcon />
*/
chatIcon?: ReactNode
/**
* CSS class name.
*/
className?: string
/**
* Aligns navigation links to the specified side.
* @default 'right'
*/
align?: 'left' | 'right'
}
// Fix compiler error
// Expression type `JSXElement` cannot be safely reordered
const defaultGitHubIcon = (
<GitHubIcon height="24" aria-label="Project repository" />
)
const defaultChatIcon = <DiscordIcon width="24" />
export const Navbar: FC<NavbarProps> = ({
pageMap,
children,
logoLink = true,
logo,
projectLink,
projectIcon = defaultGitHubIcon,
chatLink,
chatIcon = defaultChatIcon,
className,
align = 'right'
}) => {
const logoClass = cn(
'x:flex x:items-center',
align === 'left' ? 'x:max-md:me-auto' : 'x:me-auto'
)
return (
<header
className={cn(
'nextra-navbar x:sticky x:top-0 x:z-30 x:w-full x:bg-transparent x:print:hidden',
'x:max-md:[.nextra-banner:not([class$=hidden])~&]:top-(--nextra-banner-height)'
)}
>
<div
className={cn(
'nextra-navbar-blur',
'x:absolute x:-z-1 x:size-full',
'nextra-border x:border-b',
'x:backdrop-blur-md x:bg-nextra-bg/70'
)}
/>
<nav
style={{ height: 'var(--nextra-navbar-height)' }}
className={cn(
'x:mx-auto x:flex x:max-w-(--nextra-content-width) x:items-center x:gap-4 x:pl-[max(env(safe-area-inset-left),1.5rem)] x:pr-[max(env(safe-area-inset-right),1.5rem)]',
'x:justify-end',
className
)}
>
{logoLink ? (
<NextLink
href={typeof logoLink === 'string' ? logoLink : '/'}
className={cn(
logoClass,
'x:transition-opacity x:focus-visible:nextra-focus x:hover:opacity-75'
)}
aria-label="Home page"
>
{logo}
</NextLink>
) : (
<div className={logoClass}>{logo}</div>
)}
<ClientNavbar pageMap={pageMap}
className={align === 'left' ? 'x:me-auto' : ''}>
{projectLink && <Anchor href={projectLink}>{projectIcon}</Anchor>}
{chatLink && <Anchor href={chatLink}>{chatIcon}</Anchor>}
{children}
</ClientNavbar>
</nav>
</header>
)
}

View File

@@ -26,180 +26,173 @@ export default {
}, },
/* Load link with relative path */ /* Load link with relative path */
Math3200_link: { Math3200_link: {
title: 'Math 3200', title: 'Math3200',
type: 'page',
href: '/Math3200' href: '/Math3200'
}, },
Math429_link: { Math429_link: {
title: 'Math 429', title: 'Math429',
type: 'page',
href: '/Math429' href: '/Math429'
}, },
Math4111_link: { Math4111_link: {
title: 'Math 4111', title: 'Math4111',
type: 'page',
href: '/Math4111' href: '/Math4111'
}, },
Math4121_link: { Math4121_link: {
title: 'Math 4121', title: 'Math4121',
type: 'page',
href: '/Math4121' href: '/Math4121'
}, },
Math4201_link: { Math4201_link: {
title: 'Math 4201', title: 'Math4201',
type: 'page',
href: '/Math4201' href: '/Math4201'
}, },
Math416_link: { Math416_link: {
title: 'Math 416', title: 'Math416',
type: 'page',
href: '/Math416' href: '/Math416'
}, },
Math401_link: { Math401_link: {
title: 'Math 401', title: 'Math401',
type: 'page',
href: '/Math401' href: '/Math401'
}, },
CSE332S_link: { CSE332S_link: {
title: 'CSE 332S', title: 'CSE332S',
type: 'page',
href: '/CSE332S' href: '/CSE332S'
}, },
CSE347_link: { CSE347_link: {
title: 'CSE 347', title: 'CSE347',
type: 'page',
href: '/CSE347' href: '/CSE347'
}, },
CSE442T_link: { CSE442T_link: {
title: 'CSE 442T', title: 'CSE442T',
type: 'page',
href: '/CSE442T' href: '/CSE442T'
}, },
CSE5313_link: { CSE5313_link: {
title: 'CSE 5313', title: 'CSE5313',
type: 'page',
href: '/CSE5313' href: '/CSE5313'
}, },
CSE510_link: { CSE510_link: {
title: 'CSE 510', title: 'CSE510',
type: 'page',
href: '/CSE510' href: '/CSE510'
}, },
CSE559A_link: { CSE559A_link: {
title: 'CSE 559A', title: 'CSE559A',
type: 'page',
href: '/CSE559A' href: '/CSE559A'
}, },
CSE5519_link: { CSE5519_link: {
title: 'CSE 5519', title: 'CSE5519',
type: 'page',
href: '/CSE5519' href: '/CSE5519'
}, },
/* Math Courses Start */ /* Math Courses Start */
Math3200: { Math3200: {
display: 'hidden', title: 'Math3200',
type: 'page', type: 'page',
theme:{ theme:{
sidebar: false,
timestamp: true, timestamp: true,
} }
}, },
Math429:{ Math429:{
display: 'hidden', title: 'Math429',
type: 'page', type: 'page',
theme:{ theme:{
sidebar: false,
timestamp: true, timestamp: true,
} }
}, },
Math4111: { Math4111: {
display: 'hidden', title: 'Math4111',
type: 'page', type: 'page',
theme:{ theme:{
sidebar: false,
timestamp: true, timestamp: true,
} }
}, },
Math4121: { Math4121: {
display: 'hidden', title: 'Math4121',
type: 'page', type: 'page',
theme:{ theme:{
sidebar: false,
timestamp: true, timestamp: true,
} }
}, },
Math4201: { Math4201: {
display: 'hidden', title: 'Math4201',
type: 'page', type: 'page',
theme:{ theme:{
sidebar: false,
timestamp: true, timestamp: true,
} }
}, },
Math416: { Math416: {
display: 'hidden', title: 'Math416',
type: 'page', type: 'page',
theme:{ theme:{
sidebar: false,
timestamp: true, timestamp: true,
} }
}, },
Math401: { Math401: {
display: 'hidden', title: 'Math401',
type: 'page', type: 'page',
theme:{ theme:{
sidebar: false,
timestamp: true, timestamp: true,
} }
}, },
/* Math Courses End */ /* Math Courses End */
/* CSE Courses Start */ /* CSE Courses Start */
CSE332S: { CSE332S: {
display: 'hidden',
title: 'CSE332S', title: 'CSE332S',
type: 'page', type: 'page',
theme:{ theme:{
sidebar: false,
timestamp: true, timestamp: true,
} }
}, },
CSE347: { CSE347: {
display: 'hidden',
title: 'CSE347', title: 'CSE347',
type: 'page', type: 'page',
theme:{ theme:{
sidebar: false,
timestamp: true, timestamp: true,
} }
}, },
CSE442T: { CSE442T: {
display: 'hidden',
title: 'CSE442T', title: 'CSE442T',
type: 'page', type: 'page',
theme:{ theme:{
sidebar: false,
timestamp: true, timestamp: true,
} }
}, },
CSE5313: { CSE5313: {
display: 'hidden',
title: 'CSE5313', title: 'CSE5313',
type: 'page', type: 'page',
theme:{ theme:{
sidebar: false,
timestamp: true, timestamp: true,
} }
}, },
CSE510: { CSE510: {
display: 'hidden',
title: 'CSE510', title: 'CSE510',
type: 'page', type: 'page',
theme:{ theme:{
sidebar: false,
timestamp: true, timestamp: true,
} }
}, },
CSE559A: { CSE559A: {
display: 'hidden',
title: 'CSE559A', title: 'CSE559A',
type: 'page', type: 'page',
theme:{ theme:{
sidebar: false,
timestamp: true, timestamp: true,
} }
}, },
CSE5519: { CSE5519: {
display: 'hidden',
title: 'CSE5519', title: 'CSE5519',
type: 'page', type: 'page',
theme:{ theme:{
sidebar: false,
timestamp: true, timestamp: true,
} }
}, },

View File

@@ -1,10 +1,11 @@
/* eslint-env node */ /* eslint-env node */
import { Footer, Layout, Navbar } from 'nextra-theme-docs' import { Footer, Layout} from 'nextra-theme-docs'
import { Banner, Head } from 'nextra/components' import { Banner, Head } from 'nextra/components'
import { getPageMap } from 'nextra/page-map' import { getPageMap } from 'nextra/page-map'
import 'nextra-theme-docs/style.css' import 'nextra-theme-docs/style.css'
import { SpeedInsights } from "@vercel/speed-insights/next" import { SpeedInsights } from "@vercel/speed-insights/next"
import { Analytics } from "@vercel/analytics/react" import { Analytics } from "@vercel/analytics/react"
import { Navbar } from '../components/navbar'
export const metadata = { export const metadata = {
metadataBase: new URL('https://notenextra.trance-0.com'), metadataBase: new URL('https://notenextra.trance-0.com'),
@@ -27,8 +28,10 @@ export const metadata = {
} }
export default async function RootLayout({ children }) { export default async function RootLayout({ children }) {
const pageMap = await getPageMap()
const navbar = ( const navbar = (
<Navbar <Navbar
pageMap={pageMap}
logo={ logo={
<> <>
<svg width="32" height="32" viewBox="0 0 16 16"> <svg width="32" height="32" viewBox="0 0 16 16">
@@ -42,7 +45,6 @@ export default async function RootLayout({ children }) {
projectLink="https://github.com/Trance-0/NoteNextra" projectLink="https://github.com/Trance-0/NoteNextra"
/> />
) )
const pageMap = await getPageMap()
return ( return (
<html lang="en" dir="ltr" suppressHydrationWarning> <html lang="en" dir="ltr" suppressHydrationWarning>
<Head color={{ <Head color={{

View File

@@ -57,7 +57,7 @@ http {
} }
location ~ ^/Math(.*)$ { location ~ ^/Math(.*)$ {
proxy_pass http://notenextra-math/$1; proxy_pass http://notenextra-math/Math$1;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
@@ -65,7 +65,7 @@ http {
} }
location ~ ^/CSE(.*)$ { location ~ ^/CSE(.*)$ {
proxy_pass http://notenextra-cse/$1; proxy_pass http://notenextra-cse/CSE$1;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;