itpdp/web/src/routes/__root.tsx
2026-05-01 20:53:24 +02:00

152 lines
3.4 KiB
TypeScript

import { TanStackDevtools } from "@tanstack/react-devtools";
import type { QueryClient } from "@tanstack/react-query";
import {
createRootRouteWithContext,
HeadContent,
redirect,
Scripts,
} from "@tanstack/react-router";
import { TanStackRouterDevtoolsPanel } from "@tanstack/react-router-devtools";
import type * as React from "react";
import { useEffect } from "react";
import { toast } from "sonner";
import type { AuthSession } from "#/lib/auth.serverfn";
import { fetchSession, sessionQueryKey } from "#/lib/auth-client";
import { client } from "#/lib/eden";
import {
clearJoinIdFromLocation,
clearPendingPartyJoin,
getJoinIdFromLocation,
readPendingPartyJoin,
writePendingPartyJoin,
} from "#/lib/party-join";
import { Toaster } from "@/components/ui/sonner";
import TanStackQueryDevtools from "../integrations/tanstack-query/devtools";
import appCss from "../styles.css?url";
interface MyRouterContext {
queryClient: QueryClient;
}
export const Route = createRootRouteWithContext<MyRouterContext>()({
head: () => ({
meta: [
{
charSet: "utf-8",
},
{
name: "viewport",
content: "width=device-width, initial-scale=1",
},
{
title: "Music Quiz",
},
],
links: [
{
rel: "stylesheet",
href: appCss,
},
],
}),
shellComponent: RootDocument,
beforeLoad: async ({ context, location }) => {
const authPublicPaths = new Set(["/login"]);
const isAuthPublicPath = authPublicPaths.has(location.pathname);
let session: AuthSession | null;
if (typeof window === "undefined") {
const { getSession } = await import("../lib/auth.serverfn");
session = await getSession();
} else {
session = await context.queryClient.fetchQuery({
queryKey: sessionQueryKey,
queryFn: fetchSession,
staleTime: 30_000,
});
}
const user = session?.user;
if (!user && !isAuthPublicPath) {
throw redirect({
to: "/login",
search: { redirect: location.href },
});
}
return { user, session };
},
});
function RootDocument({ children }: { children: React.ReactNode }) {
const { user } = Route.useRouteContext();
useEffect(() => {
if (typeof window === "undefined") return;
const joinId = getJoinIdFromLocation();
if (!joinId) return;
const storedId = readPendingPartyJoin();
if (storedId !== joinId) {
writePendingPartyJoin(joinId);
}
clearJoinIdFromLocation();
}, []);
useEffect(() => {
if (!user) return;
const joinId = readPendingPartyJoin();
if (!joinId) return;
let cancelled = false;
const attemptJoin = async () => {
try {
const result = await client.api.party.join.post({
targetUserId: joinId,
});
if (result?.error) {
toast.error(result.error);
clearPendingPartyJoin();
} else {
toast.success("Joined party.");
clearPendingPartyJoin();
}
} catch (_error) {
if (!cancelled) {
toast.error("Failed to join party.");
}
}
};
attemptJoin();
return () => {
cancelled = true;
};
}, [user]);
return (
<html lang="en" suppressHydrationWarning>
<head>
<HeadContent />
</head>
<body className="font-sans antialiased wrap-anywhere dark">
{children}
<Toaster />
<TanStackDevtools
config={{
position: "bottom-right",
}}
plugins={[
{
name: "Tanstack Router",
render: <TanStackRouterDevtoolsPanel />,
},
TanStackQueryDevtools,
]}
/>
<Scripts />
</body>
</html>
);
}