significant refactor

This commit is contained in:
2025-08-30 01:42:23 -05:00
parent 7136f646a3
commit 052f53444e
106 changed files with 1994 additions and 1701 deletions

View File

@@ -1,59 +1,63 @@
import '@mantine/core/styles.css';
import '@mantine/dates/styles.css';
import '@mantine/carousel/styles.css';
import "@mantine/core/styles.css";
import "@mantine/dates/styles.css";
import "@mantine/carousel/styles.css";
import {
HeadContent,
Navigate,
Outlet,
Scripts,
createRootRouteWithContext
} 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, authQueryConfig } from '@/contexts/auth-context'
import Providers from '@/features/core/components/providers'
import { ColorSchemeScript, mantineHtmlProps } from '@mantine/core';
import { HeaderConfig } from '@/features/core/types/header-config';
createRootRouteWithContext,
} 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 { ColorSchemeScript, mantineHtmlProps } from "@mantine/core";
import { HeaderConfig } from "@/features/core/types/header-config";
import { playerQueries } from "@/features/players/queries";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { ensureServerQueryData } from "@/lib/tanstack-query/utils/ensure";
export const Route = createRootRouteWithContext<{
queryClient: QueryClient,
auth: AuthContextType,
header: HeaderConfig,
refresh: string[]
withPadding: boolean
queryClient: QueryClient;
auth: AuthContextType;
header: HeaderConfig;
refresh: string[];
withPadding: boolean;
}>()({
head: () => ({
meta: [
{
charSet: 'utf-8'
charSet: "utf-8",
},
{
name: 'viewport',
content: 'width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, interactive-widget=overlays-content',
}
name: "viewport",
content:
"width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, interactive-widget=overlays-content",
},
],
links: [
{
rel: 'apple-touch-icon',
sizes: '180x180',
href: '/apple-touch-icon.png',
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: "32x32",
href: "/favicon-32x32.png",
},
{
rel: 'icon',
type: 'image/png',
sizes: '16x16',
href: '/favicon-16x16.png',
rel: "icon",
type: "image/png",
sizes: "16x16",
href: "/favicon-16x16.png",
},
{ rel: 'manifest', href: '/site.webmanifest' },
{ rel: 'icon', href: '/favicon.ico' },
{ rel: "manifest", href: "/site.webmanifest" },
{ rel: "icon", href: "/favicon.ico" },
],
}),
errorComponent: (props) => {
@@ -61,25 +65,24 @@ export const Route = createRootRouteWithContext<{
<RootDocument>
<DefaultCatchBoundary {...props} />
</RootDocument>
)
);
},
component: RootComponent,
notFoundComponent: () => <Navigate to="/" />,
beforeLoad: async ({ context }) => {
// I don't really like this. I wish there was some way before the router is rendered to useAuth() and pass context there.
// See: https://github.com/TanStack/router/discussions/3531
const auth = await context.queryClient.ensureQueryData(authQueryConfig)
return {
auth
};
}
})
const auth = await ensureServerQueryData(
context.queryClient,
playerQueries.auth()
);
return { auth };
},
pendingComponent: () => <p>Loading...</p>,
});
function RootComponent() {
React.useEffect(() => {
ensureSuperTokensFrontend()
}, [])
ensureSuperTokensFrontend();
}, []);
return (
<RootDocument>
@@ -87,25 +90,38 @@ function RootComponent() {
<Outlet />
</Providers>
</RootDocument>
)
);
}
// todo: analytics -> process.env data-website-id
function RootDocument({ children }: { children: React.ReactNode }) {
return (
<html {...mantineHtmlProps} style={{ overflowX: 'hidden', overflowY: 'hidden', position: 'fixed', width: '100%' }}>
<html
{...mantineHtmlProps}
style={{
overflowX: "hidden",
overflowY: "hidden",
position: "fixed",
width: "100%",
}}
>
<head>
<HeadContent />
<ColorSchemeScript />
<link rel="stylesheet" href="/styles.css" />
<script defer src="https://analytics.yohler.net/script.js" data-website-id="0280f304-17a6-400c-8021-4d83a62d0c1b"></script>
</head>
<body style={{ overflowX: 'hidden', overflowY: 'hidden', position: 'fixed', width: '100%' }}>
<div className='app'>
{children}
</div>
<body
style={{
overflowX: "hidden",
overflowY: "hidden",
position: "fixed",
width: "100%",
}}
>
<div className="app">{children}</div>
<Scripts />
<ReactQueryDevtools />
</body>
</html>
)
);
}

View File

@@ -1,18 +1,19 @@
import { redirect, createFileRoute, Outlet } from "@tanstack/react-router";
import Layout from "@/features/core/components/layout";
import { useServerEvents } from "@/hooks/use-server-events";
import { Loader } from "@mantine/core";
export const Route = createFileRoute('/_authed')({
export const Route = createFileRoute("/_authed")({
beforeLoad: ({ context }) => {
if (!context.auth?.user) {
throw redirect({ to: '/login' })
throw redirect({ to: "/login" });
}
return {
auth: {
...context.auth,
user: context.auth.user
}
user: context.auth.user,
},
};
},
component: () => {
@@ -21,6 +22,11 @@ export const Route = createFileRoute('/_authed')({
<Layout>
<Outlet />
</Layout>
)
}
})
);
},
pendingComponent: () => (
<Layout>
<Loader size="xl" />
</Layout>
),
});

View File

@@ -1,18 +1,18 @@
import { Outlet, redirect, createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute('/_authed/admin')({
export const Route = createFileRoute("/_authed/admin")({
component: Outlet,
beforeLoad: ({ context }) => {
if (!context.auth?.roles?.includes('Admin')) {
throw redirect({ to: '/' })
if (!context.auth?.roles?.includes("Admin")) {
throw redirect({ to: "/" });
}
return {
header: {
...context.header,
title: 'Admin',
withBackButton: true
title: "Admin",
withBackButton: true,
},
};
}
})
},
});

View File

@@ -1,13 +1,13 @@
import { createFileRoute } from "@tanstack/react-router"
import { createFileRoute } from "@tanstack/react-router";
import { AdminPage } from "@/features/admin";
export const Route = createFileRoute("/_authed/admin/")({
loader: () => ({
header: {
withBackButton: true,
title: "Admin"
title: "Admin",
},
withPadding: false
withPadding: false,
}),
component: () => <AdminPage />
})
component: () => <AdminPage />,
});

View File

@@ -1,17 +1,17 @@
import { PreviewBracket } from '@/features/bracket/components/preview'
import { createFileRoute } from '@tanstack/react-router'
import { PreviewBracket } from "@/features/bracket/components/preview";
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute('/_authed/admin/preview')({
export const Route = createFileRoute("/_authed/admin/preview")({
component: RouteComponent,
loader: () => ({
header: {
withBackButton: true,
title: "Bracket Preview"
title: "Bracket Preview",
},
withPadding: false
})
})
withPadding: false,
}),
});
function RouteComponent() {
return <PreviewBracket />
return <PreviewBracket />;
}

View File

@@ -1,27 +1,31 @@
import { createFileRoute, redirect } from '@tanstack/react-router'
import { tournamentQueries } from '@/features/tournaments/queries'
import ManageTournament from '@/features/tournaments/components/manage-tournament'
import { createFileRoute, redirect } from "@tanstack/react-router";
import { tournamentQueries } from "@/features/tournaments/queries";
import ManageTournament from "@/features/tournaments/components/manage-tournament";
import { ensureServerQueryData } from "@/lib/tanstack-query/utils/ensure";
export const Route = createFileRoute('/_authed/admin/tournaments/$id')({
export const Route = createFileRoute("/_authed/admin/tournaments/$id")({
beforeLoad: async ({ context, params }) => {
const { queryClient } = context;
const tournament = await queryClient.ensureQueryData(tournamentQueries.details(params.id))
if (!tournament) throw redirect({ to: '/admin/tournaments' });
const tournament = await ensureServerQueryData(
queryClient,
tournamentQueries.details(params.id)
);
if (!tournament) throw redirect({ to: "/admin/tournaments" });
return {
tournament
}
tournament,
};
},
loader: ({ context }) => ({
header: {
withBackButton: true,
title: `Manage ${context.tournament.name}`,
},
withPadding: false
withPadding: false,
}),
component: RouteComponent,
})
});
function RouteComponent() {
const { id } = Route.useParams()
return <ManageTournament tournamentId={id} />
}
const { id } = Route.useParams();
return <ManageTournament tournamentId={id} />;
}

View File

@@ -1,12 +1,12 @@
import Page from "@/components/page";
import ManageTournaments from "@/features/admin/components/manage-tournaments";
import { tournamentQueries } from "@/features/tournaments/queries";
import { prefetchServerQuery } from "@/lib/tanstack-query/utils/prefetch";
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/_authed/admin/tournaments/")({
beforeLoad: async ({ context }) => {
const { queryClient } = context;
await queryClient.ensureQueryData(tournamentQueries.list());
await prefetchServerQuery(queryClient, tournamentQueries.list());
},
loader: () => ({
header: {
@@ -14,11 +14,11 @@ export const Route = createFileRoute("/_authed/admin/tournaments/")({
title: "Manage Tournaments",
},
refresh: tournamentQueries.list().queryKey,
withPadding: false
withPadding: false,
}),
component: RouteComponent,
});
function RouteComponent() {
return <ManageTournaments />
return <ManageTournaments />;
}

View File

@@ -1,15 +1,10 @@
import { createFileRoute } from "@tanstack/react-router";
import Page from "@/components/page";
import { TrophyIcon } from "@phosphor-icons/react";
import ListLink from "@/components/list-link";
import { tournamentQueries } from "@/features/tournaments/queries";
import { Box, Divider, Text } from "@mantine/core";
export const Route = createFileRoute("/_authed/")({
component: Home,
beforeLoad: async ({ context }) => {
await context.queryClient.ensureQueryData(tournamentQueries.list());
},
loader: () => ({
withPadding: false
})

View File

@@ -1,7 +1,7 @@
import Page from "@/components/page";
import Profile from "@/features/players/components/profile";
import { playerQueries } from "@/features/players/queries";
import { redirect, createFileRoute } from "@tanstack/react-router";
import { prefetchServerQuery } from "@/lib/tanstack-query/utils/prefetch";
import { createFileRoute } from "@tanstack/react-router";
import { z } from "zod";
const searchSchema = z.object({
@@ -12,22 +12,22 @@ export const Route = createFileRoute("/_authed/profile/$playerId")({
validateSearch: searchSchema,
beforeLoad: async ({ params, context }) => {
const { queryClient } = context;
const player = await queryClient.ensureQueryData(playerQueries.details(params.playerId))
if (!player) throw redirect({ to: '/' });
return {
player
}
await prefetchServerQuery(
queryClient,
playerQueries.details(params.playerId)
);
},
loader: ({ params, context }) => ({
header: {
collapsed: true,
withBackButton: true,
settingsLink: context?.auth.user.id === params.playerId ? 'settings' : undefined
settingsLink:
context?.auth.user.id === params.playerId ? "/settings" : undefined,
},
refresh: [playerQueries.details(params.playerId).queryKey]
refresh: [playerQueries.details(params.playerId).queryKey],
}),
component: () => {
const { player } = Route.useRouteContext();
return <Profile player={player} />
const { playerId } = Route.useParams();
return <Profile id={playerId} />;
},
})
});

View File

@@ -1,34 +1,38 @@
import { createFileRoute } from "@tanstack/react-router"
import { Box, Title, Stack } from "@mantine/core"
import { ColorSchemePicker } from "@/features/settings/components/color-scheme-picker"
import AccentColorPicker from "@/features/settings/components/accent-color-picker"
import { SignOutIcon } from "@phosphor-icons/react"
import ListLink from "@/components/list-link"
import { createFileRoute } from "@tanstack/react-router";
import { Box, Title, Stack } from "@mantine/core";
import { ColorSchemePicker } from "@/features/settings/components/color-scheme-picker";
import AccentColorPicker from "@/features/settings/components/accent-color-picker";
import { SignOutIcon } from "@phosphor-icons/react";
import ListLink from "@/components/list-link";
export const Route = createFileRoute("/_authed/settings")({
loader: () => ({
header: {
title: 'Settings',
title: "Settings",
withBackButton: true,
},
withPadding: false
withPadding: false,
}),
component: RouteComponent,
})
});
function RouteComponent() {
return <>
<Box px='md' py='sm' style={{ borderBottom: '1px solid var(--mantine-color-default-border)' }}>
<Title order={3}>Appearance</Title>
<Stack>
<AccentColorPicker />
<ColorSchemePicker />
</Stack>
</Box>
<ListLink
label='Sign Out'
to='/logout'
Icon={SignOutIcon}
/>
</>
return (
<>
<Box
px="md"
py="sm"
style={{
borderBottom: "1px solid var(--mantine-color-default-border)",
}}
>
<Title order={3}>Appearance</Title>
<Stack>
<AccentColorPicker />
<ColorSchemePicker />
</Stack>
</Box>
<ListLink label="Sign Out" to="/logout" Icon={SignOutIcon} />
</>
);
}

View File

@@ -1,6 +1,7 @@
import Page from "@/components/page";
import TeamProfile from "@/features/teams/components/team-profile";
import { teamQueries } from "@/features/teams/queries";
import { ensureServerQueryData } from "@/lib/tanstack-query/utils/ensure";
import { prefetchServerQuery } from "@/lib/tanstack-query/utils/prefetch";
import { redirect, createFileRoute } from "@tanstack/react-router";
import { z } from "zod";
@@ -12,19 +13,17 @@ export const Route = createFileRoute("/_authed/teams/$teamId")({
validateSearch: searchSchema,
beforeLoad: async ({ params, context }) => {
const { queryClient } = context;
const team = await queryClient.ensureQueryData(teamQueries.details(params.teamId))
if (!team) throw redirect({ to: '/' });
return { team }
await prefetchServerQuery(queryClient, teamQueries.details(params.teamId));
},
loader: ({ params }) => ({
header: {
collapsed: true,
withBackButton: true
withBackButton: true,
},
refresh: [teamQueries.details(params.teamId).queryKey]
refresh: [teamQueries.details(params.teamId).queryKey],
}),
component: () => {
const { team } = Route.useRouteContext();
return <TeamProfile team={team} />
const { teamId } = Route.useParams();
return <TeamProfile id={teamId} />;
},
})
});

View File

@@ -1,16 +1,8 @@
import { createFileRoute } from '@tanstack/react-router'
import { tournamentQueries } from '@/features/tournaments/queries';
import Page from '@/components/page'
import { useQuery } from '@tanstack/react-query';
import { Box, Group, Title } from '@mantine/core';
import { useSheet } from '@/hooks/use-sheet';
import Sheet from '@/components/sheet/sheet';
import { Tournament } from '@/features/tournaments/types';
import TeamList from '@/features/teams/components/team-list';
import Button from '@/components/button';
import Avatar from '@/components/avatar';
import Profile from '@/features/tournaments/components/profile';
import { z } from "zod";
import { prefetchServerQuery } from '@/lib/tanstack-query/utils/prefetch';
const searchSchema = z.object({
tab: z.string().optional(),
@@ -20,7 +12,7 @@ export const Route = createFileRoute('/_authed/tournaments/$tournamentId')({
validateSearch: searchSchema,
beforeLoad: async ({ context, params }) => {
const { queryClient } = context;
await queryClient.ensureQueryData(tournamentQueries.details(params.tournamentId))
await prefetchServerQuery(queryClient, tournamentQueries.details(params.tournamentId))
},
loader: ({ params, context }) => ({
header: {
@@ -35,6 +27,6 @@ export const Route = createFileRoute('/_authed/tournaments/$tournamentId')({
})
function RouteComponent() {
const { data: tournament } = useQuery(tournamentQueries.details(Route.useParams().tournamentId));
return <Profile tournament={tournament!} />
const tournamentId = Route.useParams().tournamentId;
return <Profile id={tournamentId} />
}

View File

@@ -2,19 +2,19 @@ import Page from '@/components/page'
import { Stack } from '@mantine/core'
import { createFileRoute } from '@tanstack/react-router'
import { TournamentCard } from '@/features/tournaments/components/tournament-card'
import { tournamentQueries } from '@/features/tournaments/queries'
import { useQuery } from '@tanstack/react-query'
import { tournamentQueries, useTournaments } from '@/features/tournaments/queries'
import { useAuth } from '@/contexts/auth-context'
import { useSheet } from '@/hooks/use-sheet'
import Sheet from '@/components/sheet/sheet'
import TournamentForm from '@/features/tournaments/components/tournament-form'
import { PlusIcon } from '@phosphor-icons/react'
import Button from '@/components/button'
import { prefetchServerQuery } from '@/lib/tanstack-query/utils/prefetch'
export const Route = createFileRoute('/_authed/tournaments/')({
beforeLoad: async ({ context }) => {
const { queryClient } = context;
await queryClient.ensureQueryData(tournamentQueries.list())
await prefetchServerQuery(queryClient, tournamentQueries.list())
},
loader: () => ({
header: {
@@ -27,7 +27,7 @@ export const Route = createFileRoute('/_authed/tournaments/')({
})
function RouteComponent() {
const { data: tournaments } = useQuery(tournamentQueries.list());
const { data: tournaments } = useTournaments();
const { roles } = useAuth();
const sheet = useSheet();

View File

@@ -1,9 +0,0 @@
import { createServerFileRoute } from '@tanstack/react-start/server';
import { superTokensRequestMiddleware } from '@/utils/supertokens';
// Simple test route for testing the auth middleware
export const ServerRoute = createServerFileRoute('/api/test').middleware([superTokensRequestMiddleware]).methods({
GET: () => {
return new Response('Hello from the authenticated API!')
},
})

View File

@@ -4,17 +4,16 @@ import { redirect, createFileRoute } from "@tanstack/react-router";
import z from "zod";
const loginSearchSchema = z.object({
stage: z.enum(['code', 'name']).optional(),
stage: z.enum(["code", "name"]).optional(),
number: z.string().optional(),
callback: z.string().optional()
callback: z.string().optional(),
});
export const Route = createFileRoute("/login")({
validateSearch: loginSearchSchema,
beforeLoad: async ({ context }) => {
if (context.auth?.user) {
throw redirect({ to: '/' })
throw redirect({ to: "/" });
}
},
component: () => {
@@ -22,6 +21,6 @@ export const Route = createFileRoute("/login")({
<LoginLayout>
<LoginFlow />
</LoginLayout>
)
}
})
);
},
});

View File

@@ -1,14 +1,15 @@
import { LoadingOverlay } from '@mantine/core'
import { signOut } from 'supertokens-web-js/recipe/passwordless'
import { redirect, createFileRoute } from '@tanstack/react-router'
import { authQueryConfig, defaultAuthData } from '@/contexts/auth-context'
import { LoadingOverlay } from "@mantine/core";
import { signOut } from "supertokens-web-js/recipe/passwordless";
import { redirect, createFileRoute } from "@tanstack/react-router";
import { defaultAuthData } from "@/contexts/auth-context";
import { playerKeys } from "@/features/players/queries";
export const Route = createFileRoute('/logout')({
export const Route = createFileRoute("/logout")({
preload: false,
loader: async ({ context }) => {
await context.queryClient.setQueryData(authQueryConfig.queryKey, defaultAuthData);
await context.queryClient.setQueryData(playerKeys.auth, defaultAuthData);
await signOut();
throw redirect({ to: '/login' });
throw redirect({ to: "/login" });
},
pendingComponent: () => <LoadingOverlay visible />
})
pendingComponent: () => <LoadingOverlay visible />,
});