81 lines
2.6 KiB
JavaScript
81 lines
2.6 KiB
JavaScript
import { Suspense, useCallback, useEffect, useRef, useState } from 'react'
|
|
import { Outlet, useLocation } from 'react-router-dom'
|
|
|
|
import Sidebar from './Sidebar'
|
|
import Topbar from './Topbar'
|
|
import AiDock from './AiDock'
|
|
import { ROUTE_BY_PATH } from './routes'
|
|
import { useBadges, useHotkeys, useNavOpen, useRouteMeta, useSidebarCollapsed } from './useShell'
|
|
import Icon from '../ui/icons'
|
|
import ErrorBoundary from '../components/ErrorBoundary'
|
|
import Spinner from '../components/Spinner'
|
|
|
|
export default function AppLayout() {
|
|
const location = useLocation()
|
|
const contentRef = useRef(null)
|
|
|
|
const [navOpen, setNavOpen] = useNavOpen()
|
|
const [collapsed, toggleCollapsed] = useSidebarCollapsed()
|
|
const [dockOpen, setDockOpen] = useState(false)
|
|
const badges = useBadges()
|
|
|
|
const routeKey = location.pathname.split('/')[1] || 'dashboard'
|
|
const route = ROUTE_BY_PATH[routeKey]
|
|
useRouteMeta(route)
|
|
|
|
const onEscape = useCallback(() => {
|
|
setNavOpen(false)
|
|
setDockOpen(false)
|
|
}, [setNavOpen])
|
|
const searchRef = useHotkeys({ onEscape })
|
|
|
|
// Navigating closes the drawer and the dock, and resets scroll — the three
|
|
// things Router.render did at the end of every route change (js/app.js:44-51).
|
|
useEffect(() => {
|
|
setNavOpen(false)
|
|
setDockOpen(false)
|
|
if (contentRef.current) contentRef.current.scrollTop = 0
|
|
}, [location.pathname, setNavOpen])
|
|
|
|
return (
|
|
<div id="app">
|
|
<a className="skip-link" href="#main-content">Skip to content</a>
|
|
<Sidebar
|
|
collapsed={collapsed}
|
|
mobileOpen={navOpen}
|
|
onToggleCollapse={toggleCollapsed}
|
|
badges={badges}
|
|
/>
|
|
|
|
<div className="main-wrap">
|
|
<Topbar onOpenNav={() => setNavOpen((o) => !o)} searchRef={searchRef} />
|
|
<main className="content" id="main-content" ref={contentRef}>
|
|
{/* Keyed by pathname: navigating away from a crashed screen resets it. */}
|
|
<ErrorBoundary key={location.pathname}>
|
|
<Suspense fallback={<div className="route-loading"><Spinner label="Loading" /></div>}>
|
|
<Outlet />
|
|
</Suspense>
|
|
</ErrorBoundary>
|
|
</main>
|
|
</div>
|
|
|
|
<button
|
|
className="ai-fab"
|
|
onClick={() => setDockOpen(true)}
|
|
title="AI Recruiter Assistant"
|
|
aria-label="Open AI Assistant"
|
|
>
|
|
<Icon name="sparkles" />
|
|
</button>
|
|
|
|
<AiDock open={dockOpen} onClose={() => setDockOpen(false)} />
|
|
|
|
<div
|
|
className={`scrim${navOpen ? ' open' : ''}`}
|
|
onClick={() => setNavOpen(false)}
|
|
aria-hidden="true"
|
|
/>
|
|
</div>
|
|
)
|
|
}
|