Files
flxn-app/src/app/routes/__root.tsx
T
yohlo 0a7b5fa00f
CI/CD Pipeline / Build and Push PocketBase Docker Image (push) Successful in 8s
CI/CD Pipeline / i18n Catalog Check (push) Failing after 10s
CI/CD Pipeline / Build and Push App Docker Image (push) Skipped
CI/CD Pipeline / Deploy to Kubernetes (push) Skipped
i18n
2026-08-08 15:29:40 -07:00

224 lines
7.4 KiB
TypeScript

import {
HeadContent,
Navigate,
Outlet,
Scripts,
createRootRouteWithContext,
isRedirect,
} from "@tanstack/react-router";
import * as React from "react";
import { DefaultCatchBoundary } from "@/components/DefaultCatchBoundary";
import { type QueryClient } from "@tanstack/react-query";
import { ensureSuperTokensFrontend } from "@/lib/supertokens/client";
import { AuthContextType } from "@/contexts/auth-context";
import Providers from "@/features/core/components/providers";
import { SessionMonitor } from "@/components/session-monitor";
import { IOSInstallPrompt } from "@/components/ios-install-prompt";
import { ColorSchemeScript, mantineHtmlProps } from "@mantine/core";
import { HeaderConfig } from "@/features/core/types/header-config";
import { playerQueries } from "@/features/players/queries";
import { ensureServerQueryData } from "@/lib/tanstack-query/utils/ensure";
import { DEFAULT_LOCALE, ensureMessages, resolveLocale } from "@/lib/i18n";
import { getRootMeta } from "@/lib/i18n/meta";
import FullScreenLoader from "@/components/full-screen-loader";
import { CHROME_COLORS } from "@/lib/mantine/theme-colors";
import mantineCssUrl from '@mantine/core/styles.css?url'
import mantineDatesCssUrl from '@mantine/dates/styles.css?url'
import mantineCarouselCssUrl from '@mantine/carousel/styles.css?url'
import mantineTiptapCssUrl from '@mantine/tiptap/styles.css?url'
export const Route = createRootRouteWithContext<{
queryClient: QueryClient;
auth: AuthContextType;
header: HeaderConfig;
refresh: string[];
withPadding: boolean;
fullWidth: boolean;
}>()({
head: () => {
// Meta stays DEFAULT_LOCALE: crawlers/unfurlers have no session, and
// reading match.context here creates circular route-type inference that
// breaks beforeLoad typing in child routes.
const rootMeta = getRootMeta(DEFAULT_LOCALE);
return {
title: "FLXN IX",
meta: [
{
charSet: "utf-8",
},
{
name: "viewport",
content:
"width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, interactive-widget=resizes-content, viewport-fit=cover",
},
{ name: 'description', content: rootMeta.description },
{ name: 'keywords', content: 'FLXN, beer pong, tournament, sports, statistics, pong' },
{ property: 'og:title', content: 'FLXN' },
{ property: 'og:description', content: rootMeta.description },
{ property: 'og:url', content: 'https://flexxon.app' },
{ property: 'og:type', content: 'website' },
{ property: 'og:site_name', content: 'FLXN' },
{ property: 'og:image', content: 'https://flexxon.app/favicon.png' },
{ property: 'og:image:width', content: '512' },
{ property: 'og:image:height', content: '512' },
{ property: 'og:image:alt', content: rootMeta.ogImageAlt },
{ property: 'og:locale', content: rootMeta.ogLocale },
{ name: 'twitter:card', content: 'summary' },
{ name: 'twitter:title', content: 'FLXN' },
{ name: 'twitter:description', content: rootMeta.description },
{ name: 'twitter:image', content: 'https://flexxon.app/favicon.png' },
{ name: 'mobile-web-app-capable', content: 'yes' },
{ name: 'apple-mobile-web-app-capable', content: 'yes' },
{ name: 'apple-mobile-web-app-status-bar-style', content: 'default' },
{ name: 'apple-mobile-web-app-title', content: 'FLXN' },
],
links: [
{
rel: "apple-touch-icon",
sizes: "180x180",
href: "/apple-touch-icon.png",
},
{
rel: "icon",
type: "image/png",
sizes: "32x32",
href: "/favicon-32x32.png",
},
{
rel: "icon",
type: "image/png",
sizes: "16x16",
href: "/favicon-16x16.png",
},
{ rel: "manifest", href: "/site.webmanifest" },
{ rel: "icon", href: "/favicon.ico" },
{ rel: 'stylesheet', href: mantineCssUrl },
{ rel: 'stylesheet', href: mantineCarouselCssUrl },
{ rel: 'stylesheet', href: mantineDatesCssUrl },
{ rel: 'stylesheet', href: mantineTiptapCssUrl },
{ rel: "preconnect", href: "https://fonts.googleapis.com" },
{
rel: "preconnect",
href: "https://fonts.gstatic.com",
crossOrigin: "anonymous",
},
{
rel: "stylesheet",
href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=League+Spartan:wght@100..900&display=swap",
}
],
};
},
errorComponent: (props) => {
return (
<RootDocument>
<Providers>
<DefaultCatchBoundary {...props} />
</Providers>
</RootDocument>
);
},
component: RootComponent,
notFoundComponent: () => <Navigate to="/" />,
beforeLoad: async ({ context, location }) => {
const publicRoutes = ['/login', '/logout', '/refresh-session'];
if (publicRoutes.some(route => location.pathname.startsWith(route))) {
return {};
}
try {
const auth = await ensureServerQueryData(
context.queryClient,
playerQueries.auth()
);
await ensureMessages(resolveLocale(auth?.metadata?.locale));
return { auth };
} catch (error: any) {
if (isRedirect(error) || error instanceof Response) throw error;
if (typeof window !== 'undefined') {
const { doesSessionExist, attemptRefreshingSession } = await import('supertokens-web-js/recipe/session');
const sessionExists = await doesSessionExist();
if (sessionExists) {
try {
await attemptRefreshingSession();
const auth = await ensureServerQueryData(
context.queryClient,
playerQueries.auth()
);
await ensureMessages(resolveLocale(auth?.metadata?.locale));
return { auth };
} catch {
return {};
}
}
}
return {};
}
},
pendingComponent: () => <Providers><FullScreenLoader /></Providers>,
});
function RootComponent() {
React.useEffect(() => {
ensureSuperTokensFrontend();
if (import.meta.env.PROD && 'serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js').catch(() => {});
}
}, []);
return (
<RootDocument>
<Providers>
<SessionMonitor />
<IOSInstallPrompt />
<Outlet />
</Providers>
</RootDocument>
);
}
// todo: analytics -> process.env data-website-id
function RootDocument({ children }: { children: React.ReactNode }) {
return (
<html
{...mantineHtmlProps}
style={{
overflowX: "hidden",
height: "100%",
width: "100%",
}}
>
<head>
<HeadContent />
<meta
name="theme-color"
media="(prefers-color-scheme: light)"
content={CHROME_COLORS.light.base}
/>
<meta
name="theme-color"
media="(prefers-color-scheme: dark)"
content={CHROME_COLORS.dark.base}
/>
<ColorSchemeScript />
<link rel="stylesheet" href="/styles.css" />
</head>
<body
style={{
overflowX: "hidden",
height: "100%",
width: "100%",
margin: 0,
padding: 0,
}}
>
<div className="app">{children}</div>
<Scripts />
</body>
</html>
);
}