Adding: themes, predictions, match reporting, ui fixes, sse fixes, badge fixes, match flow, i18n, match edits, perf, auth fix #22

Merged
kyle merged 43 commits from development into main 2026-08-23 23:09:01 -05:00
3 changed files with 72 additions and 14 deletions
Showing only changes of commit 792425a83e - Show all commits
+17 -2
View File
@@ -18,7 +18,13 @@ import { ColorSchemeScript, mantineHtmlProps } from "@mantine/core";
import { HeaderConfig } from "@/features/core/types/header-config"; import { HeaderConfig } from "@/features/core/types/header-config";
import { playerQueries } from "@/features/players/queries"; import { playerQueries } from "@/features/players/queries";
import { ensureServerQueryData } from "@/lib/tanstack-query/utils/ensure"; import { ensureServerQueryData } from "@/lib/tanstack-query/utils/ensure";
import { DEFAULT_LOCALE, ensureMessages, resolveLocale } from "@/lib/i18n"; import {
DEFAULT_LOCALE,
ensureMessages,
getCachedMessages,
i18nMessagesQueryKey,
resolveLocale,
} from "@/lib/i18n";
import { getRootMeta } from "@/lib/i18n/meta"; import { getRootMeta } from "@/lib/i18n/meta";
import FullScreenLoader from "@/components/full-screen-loader"; import FullScreenLoader from "@/components/full-screen-loader";
import { CHROME_COLORS } from "@/lib/mantine/theme-colors"; import { CHROME_COLORS } from "@/lib/mantine/theme-colors";
@@ -132,7 +138,16 @@ export const Route = createRootRouteWithContext<{
context.queryClient, context.queryClient,
playerQueries.auth() playerQueries.auth()
); );
await ensureMessages(resolveLocale(auth?.metadata?.locale)); const locale = resolveLocale(auth?.metadata?.locale);
await ensureMessages(locale);
// Dehydrate the catalog with the page so the client hydrates in the
// active locale instead of falling back to en until a chunk loads.
if (typeof window === "undefined" && locale !== DEFAULT_LOCALE) {
context.queryClient.setQueryData(
i18nMessagesQueryKey(locale),
getCachedMessages(locale)
);
}
return { auth }; return { auth };
} catch (error: any) { } catch (error: any) {
if (isRedirect(error) || error instanceof Response) throw error; if (isRedirect(error) || error instanceof Response) throw error;
+19 -1
View File
@@ -32,6 +32,8 @@ export function resolveLocale(candidate: string | null | undefined): AppLocale {
: DEFAULT_LOCALE; : DEFAULT_LOCALE;
} }
const inflightLoads: Partial<Record<AppLocale, Promise<Messages>>> = {};
export async function ensureMessages(locale: AppLocale): Promise<Messages> { export async function ensureMessages(locale: AppLocale): Promise<Messages> {
const cached = messageCache[locale]; const cached = messageCache[locale];
if (cached) return cached; if (cached) return cached;
@@ -39,11 +41,27 @@ export async function ensureMessages(locale: AppLocale): Promise<Messages> {
const loader = catalogLoaders[`/src/locales/${locale}/messages.po`]; const loader = catalogLoaders[`/src/locales/${locale}/messages.po`];
if (!loader) return messageCache[DEFAULT_LOCALE]!; if (!loader) return messageCache[DEFAULT_LOCALE]!;
const { messages } = await loader(); // Dedupe concurrent loads; a rejected load is dropped so a retry can succeed.
const pending = (inflightLoads[locale] ??= loader()
.then(({ messages }) => {
messageCache[locale] = messages; messageCache[locale] = messages;
return messages; return messages;
})
.finally(() => {
delete inflightLoads[locale];
}));
return pending;
} }
export function primeMessages(locale: AppLocale, messages: Messages) {
messageCache[locale] ??= messages;
}
// Seeded into the SSR-dehydrated query cache so hydration renders the active
// locale without depending on a client-side catalog chunk load.
export const i18nMessagesQueryKey = (locale: AppLocale) =>
["i18n-messages", locale] as const;
export function getCachedMessages(locale: AppLocale): Messages { export function getCachedMessages(locale: AppLocale): Messages {
return messageCache[locale] ?? messageCache[DEFAULT_LOCALE]!; return messageCache[locale] ?? messageCache[DEFAULT_LOCALE]!;
} }
+33 -8
View File
@@ -1,11 +1,14 @@
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { useQuery } from "@tanstack/react-query"; import { useQuery, useQueryClient } from "@tanstack/react-query";
import { I18nProvider } from "@lingui/react"; import { I18nProvider } from "@lingui/react";
import type { Messages } from "@lingui/core";
import { playerQueries } from "@/features/players/queries"; import { playerQueries } from "@/features/players/queries";
import { import {
createI18n, createI18n,
ensureMessages, ensureMessages,
hasCachedMessages, hasCachedMessages,
i18nMessagesQueryKey,
primeMessages,
resolveLocale, resolveLocale,
} from "@/lib/i18n"; } from "@/lib/i18n";
@@ -29,27 +32,49 @@ export const LinguiProvider = ({ children }: { children: React.ReactNode }) => {
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
}); });
const queryClient = useQueryClient();
const locale = resolveLocale(data?.metadata?.locale); const locale = resolveLocale(data?.metadata?.locale);
const [catalogVersion, setCatalogVersion] = useState(0); const [catalogVersion, setCatalogVersion] = useState(0);
// Fresh instance per locale (and per SSR request tree). __root beforeLoad // Fresh instance per locale (and per SSR request tree). __root beforeLoad
// preloads the catalog on authed routes; on public routes (login) it may // dehydrates the active locale's catalog with the page, so hydration primes
// not be cached yet — createI18n falls back to en until the effect below // it from the query cache and never regresses to en for the SSR'd locale.
// loads it and bumps catalogVersion. // Public routes (login) may still lack it — createI18n falls back to en
// until the effect below loads it and bumps catalogVersion.
const i18n = useMemo( const i18n = useMemo(
() => createI18n(locale), () => {
if (!hasCachedMessages(locale)) {
const seeded = queryClient.getQueryData<Messages>(
i18nMessagesQueryKey(locale)
);
if (seeded) primeMessages(locale, seeded);
}
return createI18n(locale);
},
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
[locale, catalogVersion] [locale, catalogVersion, queryClient]
); );
useEffect(() => { useEffect(() => {
if (hasCachedMessages(locale)) return; if (hasCachedMessages(locale)) return;
let cancelled = false; let cancelled = false;
ensureMessages(locale).then(() => { let retryTimer: ReturnType<typeof setTimeout> | undefined;
// A failed chunk load must not strand the UI in en — retry until the
// catalog arrives or the locale changes.
const load = () => {
ensureMessages(locale).then(
() => {
if (!cancelled) setCatalogVersion((v) => v + 1); if (!cancelled) setCatalogVersion((v) => v + 1);
}); },
() => {
if (!cancelled) retryTimer = setTimeout(load, 3000);
}
);
};
load();
return () => { return () => {
cancelled = true; cancelled = true;
if (retryTimer) clearTimeout(retryTimer);
}; };
}, [locale]); }, [locale]);