HR-ATS-Portal/frontend/src/app/AppLayout.jsx

82 lines
2.8 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 candidateView = routeKey === 'candidate'
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" className={candidateView ? 'candidate-workspace' : undefined} data-theme={candidateView ? 'dark' : undefined}>
<a className="skip-link" href="#main-content">Skip to content</a>
{(!candidateView || navOpen) && <Sidebar
collapsed={collapsed}
mobileOpen={navOpen}
onToggleCollapse={toggleCollapsed}
badges={badges}
/>}
<div className="main-wrap">
<Topbar onOpenNav={() => setNavOpen((o) => !o)} searchRef={searchRef} candidateView={candidateView} />
<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>
{!candidateView && <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>
)
}