| | | 1 | | import { |
| | | 2 | | Links, |
| | | 3 | | Meta, |
| | | 4 | | Outlet, |
| | | 5 | | Scripts, |
| | | 6 | | ScrollRestoration, |
| | | 7 | | useRouteLoaderData, |
| | | 8 | | } from "react-router"; |
| | | 9 | | |
| | | 10 | | import type { Route } from "./+types/root"; |
| | | 11 | | import { ProblemBoundary } from "~/components/problem-boundary"; |
| | | 12 | | import { SiteFooter } from "~/components/site-footer"; |
| | | 13 | | import { SiteHeader } from "~/components/site-header"; |
| | | 14 | | import { FlashToasts, Toaster, ToastProvider, type ToastInput } from "~/lib/toasts"; |
| | | 15 | | import type { PublicRuntimeConfig } from "~/public-runtime-config"; |
| | | 16 | | import type { SessionUser } from "~/lib/session.server"; |
| | | 17 | | import "./app.css"; |
| | | 18 | | |
| | | 19 | | interface RootLoaderData { |
| | | 20 | | publicRuntime: PublicRuntimeConfig; |
| | | 21 | | user: SessionUser | null; |
| | | 22 | | flashes: readonly ToastInput[]; |
| | | 23 | | } |
| | | 24 | | |
| | 0 | 25 | | export async function loader({ request }: Route.LoaderArgs): Promise<RootLoaderData> { |
| | 0 | 26 | | if (globalThis.window !== undefined) { |
| | 0 | 27 | | const w = globalThis.window as Window & { |
| | | 28 | | __CLUTTERSTOCK_PUBLIC__?: PublicRuntimeConfig; |
| | | 29 | | }; |
| | 0 | 30 | | return { |
| | | 31 | | publicRuntime: w.__CLUTTERSTOCK_PUBLIC__ ?? { |
| | | 32 | | otelTracesEndpoint: "", |
| | | 33 | | otelServiceName: "", |
| | | 34 | | }, |
| | | 35 | | user: null, |
| | | 36 | | flashes: [], |
| | | 37 | | }; |
| | | 38 | | } |
| | | 39 | | |
| | 0 | 40 | | const { getSession } = await import("~/lib/session.server"); |
| | 0 | 41 | | const { drainFlashes } = await import("~/lib/toasts.server"); |
| | 0 | 42 | | let user: SessionUser | null = null; |
| | 0 | 43 | | let flashes: readonly ToastInput[] = []; |
| | 0 | 44 | | try { |
| | 0 | 45 | | const sess = await getSession(request); |
| | 0 | 46 | | user = sess?.data.user ?? null; |
| | 0 | 47 | | flashes = await drainFlashes(request); |
| | | 48 | | } catch (err) { |
| | | 49 | | // Redis unavailable — serve page unauthenticated rather than crashing |
| | 0 | 50 | | console.error("[root] session lookup failed:", err); |
| | | 51 | | } |
| | | 52 | | |
| | 0 | 53 | | return { |
| | | 54 | | publicRuntime: { |
| | | 55 | | otelTracesEndpoint: |
| | | 56 | | process.env.PUBLIC_OTEL_EXPORTER_OTLP_TRACES_ENDPOINT?.trim() || |
| | | 57 | | process.env.VITE_OTEL_EXPORTER_OTLP_TRACES_ENDPOINT?.trim() || |
| | | 58 | | "", |
| | | 59 | | otelServiceName: |
| | | 60 | | process.env.PUBLIC_OTEL_SERVICE_NAME?.trim() || |
| | | 61 | | process.env.VITE_OTEL_SERVICE_NAME?.trim() || |
| | | 62 | | "", |
| | | 63 | | }, |
| | | 64 | | user, |
| | | 65 | | flashes, |
| | | 66 | | }; |
| | | 67 | | } |
| | | 68 | | |
| | 0 | 69 | | function PublicRuntimeConfigScript() { |
| | 0 | 70 | | const data = useRouteLoaderData("root"); |
| | 0 | 71 | | const cfg = data?.publicRuntime; |
| | 0 | 72 | | if (!cfg) return null; |
| | 0 | 73 | | const json = JSON.stringify(cfg); |
| | 0 | 74 | | return ( |
| | | 75 | | <script |
| | | 76 | | // Runs before module scripts; hydrates browser OTEL + other public runtime config. |
| | | 77 | | dangerouslySetInnerHTML={{ |
| | | 78 | | __html: `window.__CLUTTERSTOCK_PUBLIC__=${json};`, |
| | | 79 | | }} |
| | | 80 | | /> |
| | | 81 | | ); |
| | | 82 | | } |
| | | 83 | | |
| | 0 | 84 | | export const links: Route.LinksFunction = () => [ |
| | | 85 | | { rel: "icon", type: "image/svg+xml", href: "/brand/icon.svg" }, |
| | | 86 | | { rel: "icon", type: "image/png", sizes: "32x32", href: "/brand/icon-32.png" }, |
| | | 87 | | { rel: "icon", type: "image/png", sizes: "16x16", href: "/brand/icon-16.png" }, |
| | | 88 | | { rel: "apple-touch-icon", sizes: "180x180", href: "/brand/apple-touch-icon.png" }, |
| | | 89 | | { rel: "manifest", href: "/site.webmanifest" }, |
| | | 90 | | { rel: "preconnect", href: "https://fonts.googleapis.com" }, |
| | | 91 | | { |
| | | 92 | | rel: "preconnect", |
| | | 93 | | href: "https://fonts.gstatic.com", |
| | | 94 | | crossOrigin: "anonymous", |
| | | 95 | | }, |
| | | 96 | | { |
| | | 97 | | rel: "stylesheet", |
| | | 98 | | href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swa |
| | | 99 | | }, |
| | | 100 | | ]; |
| | | 101 | | |
| | 0 | 102 | | export function Layout({ |
| | | 103 | | children, |
| | | 104 | | }: { |
| | | 105 | | readonly children: React.ReactNode; |
| | | 106 | | }) { |
| | 0 | 107 | | const data = useRouteLoaderData("root") as RootLoaderData | undefined; |
| | 0 | 108 | | const initialFlashes = data?.flashes ?? []; |
| | | 109 | | |
| | 0 | 110 | | return ( |
| | | 111 | | <html lang="en" suppressHydrationWarning> |
| | | 112 | | <head> |
| | | 113 | | <meta charSet="utf-8" /> |
| | | 114 | | <meta name="viewport" content="width=device-width, initial-scale=1" /> |
| | | 115 | | {/* Set theme before first paint to avoid flash */} |
| | | 116 | | <script dangerouslySetInnerHTML={{ __html: |
| | | 117 | | `(function(){try{var t=localStorage.getItem('cs-theme');if(t&&['tui','win98','cde'].includes(t))document.docum |
| | | 118 | | }} /> |
| | | 119 | | <Meta /> |
| | | 120 | | <Links /> |
| | | 121 | | </head> |
| | | 122 | | <body className="min-h-screen flex flex-col"> |
| | | 123 | | <ToastProvider> |
| | | 124 | | <FlashToasts flashes={initialFlashes} /> |
| | | 125 | | <SiteHeader /> |
| | | 126 | | <div className="flex-1 flex flex-col">{children}</div> |
| | | 127 | | <SiteFooter /> |
| | | 128 | | <Toaster /> |
| | | 129 | | </ToastProvider> |
| | | 130 | | <PublicRuntimeConfigScript /> |
| | | 131 | | <ScrollRestoration /> |
| | | 132 | | <Scripts /> |
| | | 133 | | </body> |
| | | 134 | | </html> |
| | | 135 | | ); |
| | | 136 | | } |
| | | 137 | | |
| | 0 | 138 | | export default function App() { |
| | 0 | 139 | | return <Outlet />; |
| | | 140 | | } |
| | | 141 | | |
| | 0 | 142 | | export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) { |
| | 0 | 143 | | return ( |
| | | 144 | | <main className="pt-16 p-4 container mx-auto"> |
| | | 145 | | <ProblemBoundary error={error} scope="page" /> |
| | | 146 | | </main> |
| | | 147 | | ); |
| | | 148 | | } |