Skip to content
File

Blob: src/client/components/app-shell.tsx

typescript44 lines
1import { Suspense } from "react";
2import { Outlet, useLocation } from "react-router-dom";
3import { useAuth } from "@/client/auth";
4import { AppConfigErrorPage } from "@/client/components/app-config-error-page";
5import { ErrorBoundary } from "@/client/components/error-boundary";
6import { Header } from "@/client/components/header";
7import { Footer } from "@/client/components/footer";
8import { LoadingPanel } from "@/client/components/loading-panel";
9import { useRouteAnnouncer } from "@/client/hooks/use-route-announcer";
10 
11export const AppShell = () => {
12 const { pathname } = useLocation();
13 const { startupError } = useAuth();
14 const isLandingRoute = pathname === "/";
15 const announcerRef = useRouteAnnouncer();
16 
17 return (
18 <div className="relative z-10 flex min-h-screen flex-col sm:h-screen">
19 <a
20 href="#main-content"
21 className="sr-only focus:not-sr-only focus:fixed focus:top-4 focus:left-4 focus:z-[200] focus:rounded-lg focus:bg-zinc-900 focus:px-4 focus:py-2 focus:text-accent-400 focus:ring-2 focus:ring-accent-500/50"
22 >
23 Skip to content
24 </a>
25 <Header variant={isLandingRoute ? "public" : "app"} />
26 <main id="main-content" className={`flex-1 sm:overflow-y-auto${isLandingRoute ? " overflow-x-clip" : ""}`}>
27 <div className={isLandingRoute ? undefined : "mx-auto max-w-7xl px-4 py-6 sm:px-6"}>
28 <ErrorBoundary>
29 {startupError ? (
30 <AppConfigErrorPage message={startupError.message} />
31 ) : (
32 <Suspense fallback={<LoadingPanel label="Loading page..." />}>
33 <Outlet />
34 </Suspense>
35 )}
36 </ErrorBoundary>
37 </div>
38 </main>
39 <Footer />
40 <div ref={announcerRef} className="sr-only" aria-live="polite" />
41 </div>
42 );
43};