Files
supabase/apps/docs/layouts/MainSkeleton.tsx
Chris Chinchilla e01699cce5 docs: Further toggles for Nimbus project (#39010)
chore: feature flag footer and fdw section

Co-authored-by: Charis Lam <26616127+charislam@users.noreply.github.com>
2025-09-25 14:26:46 +00:00

434 lines
11 KiB
TypeScript

'use client'
import dynamic from 'next/dynamic'
import { usePathname } from 'next/navigation'
import { memo, type PropsWithChildren, type ReactNode, useEffect } from 'react'
// End of third-party imports
import { isFeatureEnabled } from 'common'
import { cn } from 'ui'
import type { NavMenuSection } from '~/components/Navigation/Navigation.types'
import DefaultNavigationMenu, {
type MenuId,
} from '~/components/Navigation/NavigationMenu/NavigationMenu'
import { getMenuId } from '~/components/Navigation/NavigationMenu/NavigationMenu.utils'
import TopNavBar from '~/components/Navigation/NavigationMenu/TopNavBar'
import { DOCS_CONTENT_CONTAINER_ID } from '~/features/ui/helpers.constants'
import { menuState, useMenuMobileOpen } from '~/hooks/useMenuState'
const Footer = dynamic(() => import('~/components/Navigation/Footer'))
const footerEnabled = isFeatureEnabled('docs:footer')
const levelsData = {
home: {
icon: 'home',
name: 'Home',
},
gettingstarted: {
icon: 'getting-started',
name: 'Getting Started',
},
database: {
icon: 'database',
name: 'Database',
},
cron: {
icon: 'cron',
name: 'Cron',
},
queues: {
icon: 'queues',
name: 'Queues',
},
api: {
icon: 'rest',
name: 'REST API',
},
graphql: {
icon: 'graphql',
name: 'GraphQL',
},
auth: {
icon: 'auth',
name: 'Auth',
},
functions: {
icon: 'edge-functions',
name: 'Edge Functions',
},
telemetry: {
icon: 'telemetry',
name: 'Telemetry',
},
realtime: {
icon: 'realtime',
name: 'Realtime',
},
analytics: {
icon: 'analytics',
name: 'Analytics',
},
storage: {
icon: 'storage',
name: 'Storage',
},
ai: {
icon: 'ai',
name: 'AI & Vectors',
},
local_development: {
icon: 'reference-cli',
name: 'Local Development',
},
security: {
icon: 'platform',
name: 'Security',
},
platform: {
icon: 'platform',
name: 'Platform',
},
contributing: {
icon: 'contributing',
name: 'Contributing',
},
resources: {
icon: 'resources',
name: 'Resources',
},
self_hosting: {
icon: 'self-hosting',
name: 'Self-Hosting',
},
integrations: {
icon: 'integrations',
name: 'Integrations',
},
reference_javascript_v1: {
icon: 'reference-javascript',
name: 'Javascript Reference v1.0',
},
reference_javascript_v2: {
icon: 'reference-javascript',
name: 'Javascript Reference v2.0',
},
reference_dart_v1: {
icon: 'reference-dart',
name: 'Dart Reference v1.0',
},
reference_dart_v2: {
icon: 'reference-dart',
name: 'Dart Reference v2.0',
},
reference_csharp_v0: {
icon: 'reference-csharp',
name: 'C# Reference v0.0',
},
reference_csharp_v1: {
icon: 'reference-csharp',
name: 'C# Reference v1.0',
},
reference_python_v2: {
icon: 'reference-python',
name: 'Python Reference v2.0',
},
reference_swift_v1: {
icon: 'reference-swift',
name: 'Swift Reference v1.0',
},
reference_swift_v2: {
icon: 'reference-swift',
name: 'Swift Reference v2.0',
},
reference_kotlin_v1: {
icon: 'reference-kotlin',
name: 'Kotlin Reference v1.0',
},
reference_kotlin_v2: {
icon: 'reference-kotlin',
name: 'Kotlin Reference v2.0',
},
reference_kotlin_v3: {
icon: 'reference-kotlin',
name: 'Kotlin Reference v3.0',
},
reference_cli: {
icon: 'reference-cli',
name: 'CLI Reference',
},
reference_api: {
icon: 'reference-api',
name: 'Management API Reference',
},
reference_self_hosting_auth: {
icon: 'reference-auth',
name: 'Auth Server Reference',
},
reference_self_hosting_storage: {
icon: 'reference-storage',
name: 'Storage Server Reference',
},
reference_self_hosting_realtime: {
icon: 'reference-realtime',
name: 'Realtime Server Reference',
},
reference_self_hosting_analytics: {
icon: 'reference-analytics',
name: 'Analytics Server Reference',
},
reference_self_hosting_functions: {
icon: 'reference-functions',
name: 'Functions Server Reference',
},
}
type MobileHeaderProps = { menuId: MenuId } | { menuName: string }
const MobileHeader = memo(function MobileHeader(props: MobileHeaderProps) {
const mobileMenuOpen = useMenuMobileOpen()
return (
<div
className={cn(
'lg:hidden px-3.5 border-b z-10',
'transition-all ease-out',
'top-0',
mobileMenuOpen && 'absolute',
'flex items-center',
mobileMenuOpen ? 'gap-0' : 'gap-1'
)}
>
<button
className={cn(
'h-8 w-8 flex group items-center justify-center mr-1',
mobileMenuOpen && 'mt-0.5'
)}
onClick={() => menuState.setMenuMobileOpen(!mobileMenuOpen)}
>
<div
className={cn(
'space-y-1 cursor-pointer relative',
mobileMenuOpen ? 'w-4 h-4' : 'w-4 h-[8px]'
)}
>
<span
className={cn(
'transition-all ease-out block w-4 h-px bg-foreground-muted group-hover:bg-foreground',
!mobileMenuOpen ? 'w-4' : 'absolute rotate-45 top-[6px]'
)}
/>
<span
className={cn(
'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]'
)}
/>
</div>
</button>
<span
className={cn(
'transition-all duration-200',
'text-foreground',
mobileMenuOpen ? 'text-xs' : 'text-sm'
)}
>
{mobileMenuOpen
? 'Close'
: 'menuId' in props
? levelsData[props.menuId]?.name ?? levelsData['home'].name
: props.menuName}
</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={cn(
'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'
)}
onClick={() => menuState.setMenuMobileOpen(!mobileMenuOpen)}
></div>
)
})
const Container = memo(function Container({
children,
className,
}: PropsWithChildren<{ className?: string }>) {
return (
<main
// used by layout to scroll to top
id={DOCS_CONTENT_CONTAINER_ID}
className={cn(
'w-full transition-all ease-out relative',
// desktop override any margin styles
'lg:ml-0',
className
)}
>
<div className="flex flex-col sticky top-0">{children}</div>
</main>
)
})
const NavContainer = memo(function NavContainer({ children }: PropsWithChildren) {
const mobileMenuOpen = useMenuMobileOpen()
return (
<nav
aria-labelledby="main-nav-title"
className={cn(
'fixed lg:relative z-40 lg:z-auto',
mobileMenuOpen ? 'w-[75%] sm:w-[50%] md:w-[33%] left-0' : 'w-0 -left-full',
'lg:w-[420px] !lg:left-0',
'lg:top-[var(--header-height)] lg:sticky',
'h-screen lg:h-[calc(100vh-var(--header-height))]',
// desktop override any left styles
'lg:left-0',
'transition-all',
'top-0 bottom-0',
'flex flex-col ml-0',
'border-r',
'lg:overflow-y-auto'
)}
>
<div
className={cn(
'top-0 lg:top-[var(--header-height)]',
'h-full',
'relative lg:sticky',
'w-full lg:w-auto',
'h-fit lg:h-screen overflow-y-scroll lg:overflow-auto',
'[overscroll-behavior:contain]',
'backdrop-blur backdrop-filter bg-background',
'flex flex-col flex-grow'
)}
>
<span id="main-nav-title" className="sr-only">
Main menu
</span>
<div className="top-0 sticky h-0 z-10">
<div className="bg-gradient-to-b from-background to-transparent h-4 w-full"></div>
</div>
<div
className={cn(
'transition-all ease-out duration-200',
'absolute left-0 right-0',
'px-5 pl-5 pt-6 pb-16 lg:pb-32',
'bg-background',
// desktop styles
'lg:relative lg:left-0 lg:pb-10 lg:px-10 lg:flex',
'lg:opacity-100 lg:visible'
)}
>
{children}
</div>
</div>
</nav>
)
})
interface SkeletonProps extends PropsWithChildren {
menuId?: MenuId
menuName?: string
hideSideNav?: boolean
NavigationMenu?: ReactNode
hideFooter?: boolean
className?: string
additionalNavItems?: Record<string, Partial<NavMenuSection>[]>
}
function TopNavSkeleton({ children }) {
return (
<div className="flex flex-col h-full w-full">
<div className="hidden lg:sticky w-full lg:flex top-0 left-0 right-0 z-50">
<TopNavBar />
</div>
{children}
</div>
)
}
function SidebarSkeleton({
children,
menuId: _menuId,
menuName,
NavigationMenu,
hideFooter = !footerEnabled,
className,
hideSideNav,
additionalNavItems,
}: SkeletonProps) {
const pathname = usePathname()
const menuId = _menuId ?? getMenuId(pathname)
const mobileMenuOpen = useMenuMobileOpen()
return (
<div className={cn('flex flex-row h-full relative', className)}>
{!hideSideNav && (
<NavContainer>
{NavigationMenu ?? (
<DefaultNavigationMenu menuId={menuId} additionalNavItems={additionalNavItems} />
)}
</NavContainer>
)}
<Container>
<div
className={cn(
'flex lg:hidden w-full top-0 left-0 right-0 z-50',
hideSideNav && 'sticky',
mobileMenuOpen && 'z-10'
)}
>
<TopNavBar />
</div>
<div
className={cn(
'sticky',
'transition-all top-0 z-10',
'backdrop-blur backdrop-filter bg-background'
)}
>
{hideSideNav ? null : menuName ? (
<MobileHeader menuName={menuName} />
) : (
<MobileHeader menuId={menuId} />
)}
</div>
<div className="grow">
{children}
{!hideFooter && <Footer />}
</div>
<MobileMenuBackdrop />
</Container>
</div>
)
}
export { SidebarSkeleton, TopNavSkeleton }