Files
supabase/apps/docs/layouts/MainSkeleton.tsx
T
Charis 7cf2eae953 fix: statically build the right nav on each page (#21469)
Before: All pages have the Home nav menu in static HTML, which is blown away and replaced by the proper nav menu upon hydration. This leads to jankiness when the page first loads and an unpleasant flash of the wrong nav menu (especially obvious on the JavaScript ref page, which takes a long time to process and rerender the nav).

Now: All pages have their correct nav menu in static HTML.
2024-02-23 11:38:06 -05:00

377 lines
10 KiB
TypeScript

import { useTheme } from 'next-themes'
import Image from 'next/legacy/image'
import Link from 'next/link'
import NavigationMenu, { type MenuId } from '~/components/Navigation/NavigationMenu/NavigationMenu'
import TopNavBar from '~/components/Navigation/NavigationMenu/TopNavBar'
import { PropsWithChildren, memo, useEffect } from 'react'
import Footer from '~/components/Navigation/Footer'
import { menuState, useMenuMobileOpen } from '~/hooks/useMenuState'
const levelsData = {
home: {
icon: '/docs/img/icons/menu/home',
name: 'Home',
},
gettingstarted: {
icon: '/docs/img/icons/menu/getting-started',
name: 'Getting Started',
},
database: {
icon: '/docs/img/icons/menu/database',
name: 'Database',
},
api: {
icon: '/docs/img/icons/menu/rest',
name: 'REST API',
},
graphql: {
icon: '/docs/img/icons/menu/graphql',
name: 'GraphQL',
},
auth: {
icon: '/docs/img/icons/menu/auth',
name: 'Auth',
},
functions: {
icon: '/docs/img/icons/menu/functions',
name: 'Edge Functions',
},
realtime: {
icon: '/docs/img/icons/menu/realtime',
name: 'Realtime',
},
analytics: {
icon: '/docs/img/icons/menu/analytics',
name: 'Analytics',
},
storage: {
icon: '/docs/img/icons/menu/storage',
name: 'Storage',
},
ai: {
icon: '/docs/img/icons/menu/ai',
name: 'AI & Vectors',
},
supabase_cli: {
icon: '/docs/img/icons/menu/reference-cli',
name: 'Supabase CLI',
},
platform: {
icon: '/docs/img/icons/menu/platform',
name: 'Platform',
},
resources: {
icon: '/docs/img/icons/menu/resources',
name: 'Resources',
},
self_hosting: {
icon: '/docs/img/icons/menu/self-hosting',
name: 'Self-Hosting',
},
integrations: {
icon: '/docs/img/icons/menu/integrations',
name: 'Integrations',
},
reference_javascript_v1: {
icon: '/docs/img/icons/menu/reference-javascript',
name: 'Javascript Reference v1.0',
},
reference_javascript_v2: {
icon: '/docs/img/icons/menu/reference-javascript',
name: 'Javascript Reference v2.0',
},
reference_dart_v1: {
icon: '/docs/img/icons/menu/reference-dart',
name: 'Dart Reference v1.0',
},
reference_dart_v2: {
icon: '/docs/img/icons/menu/reference-dart',
name: 'Dart Reference v2.0',
},
reference_csharp_v0: {
icon: '/docs/img/icons/menu/reference-csharp',
name: 'C# Reference v0.0',
},
reference_python_v2: {
icon: '/docs/img/icons/menu/reference-python',
name: 'Python Reference v2.0',
},
reference_swift_v1: {
icon: '/docs/img/icons/menu/reference-swift',
name: 'Swift Reference v1.0',
},
reference_swift_v2: {
icon: '/docs/img/icons/menu/reference-swift',
name: 'Swift Reference v2.0',
},
reference_kotlin_v1: {
icon: '/docs/img/icons/menu/reference-kotlin',
name: 'Kotlin Reference v1.0',
},
reference_kotlin_v2: {
icon: '/docs/img/icons/menu/reference-kotlin',
name: 'Kotlin Reference v2.0',
},
reference_cli: {
icon: '/docs/img/icons/menu/reference-cli',
name: 'CLI Reference',
},
reference_api: {
icon: '/docs/img/icons/menu/reference-api',
name: 'Management API Reference',
},
reference_self_hosting_auth: {
icon: '/docs/img/icons/menu/reference-auth',
name: 'Auth Server Reference',
},
reference_self_hosting_storage: {
icon: '/docs/img/icons/menu/reference-storage',
name: 'Storage Server Reference',
},
reference_self_hosting_realtime: {
icon: '/docs/img/icons/menu/reference-realtime',
name: 'Realtime Server Reference',
},
reference_self_hosting_analytics: {
icon: '/docs/img/icons/menu/reference-analytics',
name: 'Analytics Server Reference',
},
reference_self_hosting_functions: {
icon: '/docs/img/icons/menu/reference-functions',
name: 'Functions Server Reference',
},
}
const MobileHeader = memo(function MobileHeader({ menuId }: { menuId: MenuId }) {
const mobileMenuOpen = useMenuMobileOpen()
const menuLevel = menuId
return (
<div
className={[
'transition-all ease-out z-10',
'top-0',
mobileMenuOpen && 'absolute',
'flex items-center h-[40px]',
mobileMenuOpen ? 'gap-0' : 'gap-3',
].join(' ')}
>
<button
className={['mr-2', mobileMenuOpen && 'mt-0.5'].join(' ')}
onClick={() => menuState.setMenuMobileOpen(!mobileMenuOpen)}
>
<div
className={[
'space-y-1 group cursor-pointer relative',
mobileMenuOpen ? 'w-4 h-4' : 'w-4 h-[8px]',
].join(' ')}
>
<span
className={[
'transition-all ease-out block w-4 h-px bg-foreground-muted group-hover:bg-foreground',
!mobileMenuOpen ? 'w-4' : 'absolute rotate-45 top-[6px]',
].join(' ')}
></span>
<span
className={[
'transition-all ease-out block h-px bg-foreground-muted group-hover:bg-foreground',
!mobileMenuOpen ? 'w-3 group-hover:w-4' : 'absolute w-4 -rotate-45 top-[2px]',
].join(' ')}
></span>
</div>
</button>
<div className={[].join(' ')}>
<img
src={menuLevel ? levelsData[menuLevel].icon + '.svg' : levelsData['home'].icon + '.svg'}
className={[
'transition-all duration-200',
mobileMenuOpen ? 'invisible w-0 h-0' : 'w-4 h-4',
].join(' ')}
/>
</div>
<span
className={[
'transition-all duration-200',
'text-foreground',
mobileMenuOpen ? 'text-xs' : 'text-sm',
].join(' ')}
>
{mobileMenuOpen
? 'Close'
: menuLevel
? levelsData[menuLevel].name
: levelsData['home'].name}
</span>
</div>
)
})
const MobileMenuBackdrop = memo(function MobileMenuBackdrop() {
const mobileMenuOpen = useMenuMobileOpen()
useEffect(() => {
window.addEventListener('resize', (e: UIEvent) => {
const w = e.target as Window
if (mobileMenuOpen && w.innerWidth >= 1024) {
menuState.setMenuMobileOpen(!mobileMenuOpen)
}
})
return () => {
window.removeEventListener('resize', () => {})
}
}, [mobileMenuOpen])
return (
<div
className={[
'h-full',
'left-0',
'right-0',
'z-10',
'backdrop-blur-sm backdrop-filter bg-alternative/90',
mobileMenuOpen ? 'absolute h-full w-full top-0 left-0' : 'hidden h-0',
// always hide on desktop
'lg:hidden',
].join(' ')}
onClick={() => menuState.setMenuMobileOpen(!mobileMenuOpen)}
></div>
)
})
const HeaderLogo = memo(function HeaderLogo() {
const { resolvedTheme } = useTheme()
return (
<Link href="/" className="px-10 flex items-center gap-2">
<Image
className="cursor-pointer"
src={
resolvedTheme?.includes('dark') ? '/docs/supabase-dark.svg' : '/docs/supabase-light.svg'
}
width={96}
height={24}
alt="Supabase Logo"
/>
<span className="font-mono text-sm font-medium text-brand-link">DOCS</span>
</Link>
)
})
const Container = memo(function Container(props: PropsWithChildren) {
const mobileMenuOpen = useMenuMobileOpen()
return (
<div
// #docs-content-container is used by layout to scroll to top
id="docs-content-container"
className={[
// 'overflow-x-auto',
'w-full transition-all ease-out',
// 'absolute lg:relative',
mobileMenuOpen ? 'ml-[75%] sm:ml-[50%] md:ml-[33%] overflow-hidden' : 'overflow-auto',
// desktop override any margin styles
'lg:ml-0',
].join(' ')}
>
<div className="flex flex-col relative">{props.children}</div>
</div>
)
})
const NavContainer = memo(function NavContainer({ menuId }: { menuId: MenuId }) {
const mobileMenuOpen = useMenuMobileOpen()
return (
<nav
aria-labelledby="main-nav-title"
className={[
// 'hidden',
'absolute lg:relative',
mobileMenuOpen ? 'w-[75%] sm:w-[50%] md:w-[33%] left-0' : 'w-0 -left-[280px]',
'lg:w-[420px] !lg:left-0',
// desktop override any left styles
'lg:left-0',
'transition-all',
'top-0',
'flex flex-col ml-0',
].join(' ')}
>
<div
className={[
'top-0',
'relative',
'w-auto',
'border-r overflow-auto h-screen',
'backdrop-blur backdrop-filter bg-background',
'flex flex-col',
].join(' ')}
>
<h1 id="main-nav-title" className="sr-only">
Main menu
</h1>
<div className="top-0 sticky z-10">
<div>
<div>
<div
className={[
'hidden lg:flex lg:height-auto',
'pt-8 bg-background flex-col gap-8',
].join(' ')}
>
<HeaderLogo />
</div>
<div className="h-4 bg-background w-full"></div>
<div className="bg-gradient-to-b from-background to-transparent h-4 w-full"></div>
</div>
</div>
</div>
<div
className={[
'transition-all ease-out duration-200',
'absolute left-0 right-0 h-screen',
'px-5 pl-5 py-16',
'top-[0px]',
'bg-background',
// desktop styles
'lg:relative lg:top-0 lg:left-0 lg:pb-10 lg:px-10 lg:pt-0 lg:flex',
'lg:opacity-100 lg:visible',
].join(' ')}
>
<NavigationMenu menuId={menuId} />
</div>
</div>
</nav>
)
})
function MainSkeleton({ children, menuId }: PropsWithChildren<{ menuId: MenuId }>) {
return (
<div className="flex flex-row h-full">
<NavContainer menuId={menuId} />
<Container>
<div className={['lg:sticky top-0 z-10 overflow-hidden'].join(' ')}>
<TopNavBar />
</div>
<div
className={[
'sticky transition-all top-0',
'z-10',
'backdrop-blur backdrop-filter bg-background',
].join(' ')}
>
<div className={['lg:hidden', 'px-5 ', 'border-b z-10'].join(' ')}>
<MobileHeader menuId={menuId} />
</div>
</div>
<div className="grow">
{children}
<Footer />
</div>
<MobileMenuBackdrop />
</Container>
</div>
)
}
export { MainSkeleton }