Skip to content
File

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

typescript60 lines
1import { Outlet, useLocation } from "react-router";
2 
3import { ErrorBoundary } from "@/client/components/error-boundary";
4import { Footer } from "@/client/components/footer";
5import { Header } from "@/client/components/header";
6import { useSessionWithInitialPending } from "@/client/lib/auth-client";
7import { useSessionQueryCacheBoundary } from "@/client/lib/query";
8 
9// Pure unauthenticated paths — sign-in, auth error, the invite acceptance
10// flow. `/` is auth-aware (marketing vs launcher) so it's not in this
11// set; the header variant for `/` is chosen by session state below.
12const ALWAYS_PUBLIC_PATHS = ["/sign-in", "/auth-error", "/privacy", "/terms"];
13 
14const isPublicPath = (pathname: string): boolean => {
15 if (ALWAYS_PUBLIC_PATHS.includes(pathname)) return true;
16 if (pathname.startsWith("/invite/")) return true;
17 return false;
18};
19 
20export const AppShell = () => {
21 const { pathname } = useLocation();
22 const session = useSessionWithInitialPending();
23 useSessionQueryCacheBoundary(session.data?.user.id ?? null, session.isPending);
24 // Public chrome (brand + sign-in pill) for unauthenticated paths and
25 // for the marketing landing when no session exists. `/` keeps neutral
26 // loading chrome only during the initial auth resolution so later
27 // refetches don't swap mounted content back to a placeholder.
28 const variant =
29 pathname === "/" && session.isInitialPending
30 ? "loading"
31 : isPublicPath(pathname) || (pathname === "/" && !session.data)
32 ? "public"
33 : "app";
34 
35 return (
36 <div className="relative z-10 flex min-h-screen flex-col">
37 <a
38 href="#main-content"
39 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"
40 >
41 Skip to content
42 </a>
43 <Header variant={variant} />
44 <main id="main-content" className="flex-1">
45 <div className="mx-auto max-w-7xl px-4 py-6 sm:px-6">
46 {/* Resetting on `pathname` means a runtime error on /account/apps
47 clears as soon as the user navigates away — no full page
48 reload needed to recover. */}
49 <ErrorBoundary resetKey={pathname}>
50 <div className="animate-slide-up">
51 <Outlet />
52 </div>
53 </ErrorBoundary>
54 </div>
55 </main>
56 <Footer />
57 </div>
58 );
59};