File
Blob: src/client/components/app-shell.tsx
| 1 | import { Outlet, useLocation } from "react-router"; |
| 2 | |
| 3 | import { ErrorBoundary } from "@/client/components/error-boundary"; |
| 4 | import { Footer } from "@/client/components/footer"; |
| 5 | import { Header } from "@/client/components/header"; |
| 6 | import { useSessionWithInitialPending } from "@/client/lib/auth-client"; |
| 7 | import { 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. |
| 12 | const ALWAYS_PUBLIC_PATHS = ["/sign-in", "/auth-error", "/privacy", "/terms"]; |
| 13 | |
| 14 | const 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 | |
| 20 | export 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 | }; |