This commit is contained in:
yohlo
2025-08-20 22:35:40 -05:00
commit f51c278cd3
169 changed files with 8173 additions and 0 deletions

391
src/app/routeTree.gen.ts Normal file
View File

@@ -0,0 +1,391 @@
/* eslint-disable */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// This file was automatically generated by TanStack Router.
// You should NOT make any changes in this file as it will be overwritten.
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { createServerRootRoute } from '@tanstack/react-start/server'
import { Route as rootRouteImport } from './routes/__root'
import { Route as LogoutRouteImport } from './routes/logout'
import { Route as LoginRouteImport } from './routes/login'
import { Route as AuthedRouteImport } from './routes/_authed'
import { Route as AuthedIndexRouteImport } from './routes/_authed/index'
import { Route as AuthedSettingsRouteImport } from './routes/_authed/settings'
import { Route as AuthedAdminRouteImport } from './routes/_authed/admin'
import { Route as AuthedTournamentsIndexRouteImport } from './routes/_authed/tournaments/index'
import { Route as AuthedAdminIndexRouteImport } from './routes/_authed/admin/index'
import { Route as AuthedTournamentsTournamentIdRouteImport } from './routes/_authed/tournaments/$tournamentId'
import { Route as AuthedTeamsTeamIdRouteImport } from './routes/_authed/teams.$teamId'
import { Route as AuthedProfilePlayerIdRouteImport } from './routes/_authed/profile.$playerId'
import { Route as AuthedAdminPreviewRouteImport } from './routes/_authed/admin/preview'
import { ServerRoute as ApiTestServerRouteImport } from './routes/api/test'
import { ServerRoute as ApiEventsSplatServerRouteImport } from './routes/api/events.$'
import { ServerRoute as ApiAuthSplatServerRouteImport } from './routes/api/auth.$'
const rootServerRouteImport = createServerRootRoute()
const LogoutRoute = LogoutRouteImport.update({
id: '/logout',
path: '/logout',
getParentRoute: () => rootRouteImport,
} as any)
const LoginRoute = LoginRouteImport.update({
id: '/login',
path: '/login',
getParentRoute: () => rootRouteImport,
} as any)
const AuthedRoute = AuthedRouteImport.update({
id: '/_authed',
getParentRoute: () => rootRouteImport,
} as any)
const AuthedIndexRoute = AuthedIndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => AuthedRoute,
} as any)
const AuthedSettingsRoute = AuthedSettingsRouteImport.update({
id: '/settings',
path: '/settings',
getParentRoute: () => AuthedRoute,
} as any)
const AuthedAdminRoute = AuthedAdminRouteImport.update({
id: '/admin',
path: '/admin',
getParentRoute: () => AuthedRoute,
} as any)
const AuthedTournamentsIndexRoute = AuthedTournamentsIndexRouteImport.update({
id: '/tournaments/',
path: '/tournaments/',
getParentRoute: () => AuthedRoute,
} as any)
const AuthedAdminIndexRoute = AuthedAdminIndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => AuthedAdminRoute,
} as any)
const AuthedTournamentsTournamentIdRoute =
AuthedTournamentsTournamentIdRouteImport.update({
id: '/tournaments/$tournamentId',
path: '/tournaments/$tournamentId',
getParentRoute: () => AuthedRoute,
} as any)
const AuthedTeamsTeamIdRoute = AuthedTeamsTeamIdRouteImport.update({
id: '/teams/$teamId',
path: '/teams/$teamId',
getParentRoute: () => AuthedRoute,
} as any)
const AuthedProfilePlayerIdRoute = AuthedProfilePlayerIdRouteImport.update({
id: '/profile/$playerId',
path: '/profile/$playerId',
getParentRoute: () => AuthedRoute,
} as any)
const AuthedAdminPreviewRoute = AuthedAdminPreviewRouteImport.update({
id: '/preview',
path: '/preview',
getParentRoute: () => AuthedAdminRoute,
} as any)
const ApiTestServerRoute = ApiTestServerRouteImport.update({
id: '/api/test',
path: '/api/test',
getParentRoute: () => rootServerRouteImport,
} as any)
const ApiEventsSplatServerRoute = ApiEventsSplatServerRouteImport.update({
id: '/api/events/$',
path: '/api/events/$',
getParentRoute: () => rootServerRouteImport,
} as any)
const ApiAuthSplatServerRoute = ApiAuthSplatServerRouteImport.update({
id: '/api/auth/$',
path: '/api/auth/$',
getParentRoute: () => rootServerRouteImport,
} as any)
export interface FileRoutesByFullPath {
'/login': typeof LoginRoute
'/logout': typeof LogoutRoute
'/admin': typeof AuthedAdminRouteWithChildren
'/settings': typeof AuthedSettingsRoute
'/': typeof AuthedIndexRoute
'/admin/preview': typeof AuthedAdminPreviewRoute
'/profile/$playerId': typeof AuthedProfilePlayerIdRoute
'/teams/$teamId': typeof AuthedTeamsTeamIdRoute
'/tournaments/$tournamentId': typeof AuthedTournamentsTournamentIdRoute
'/admin/': typeof AuthedAdminIndexRoute
'/tournaments': typeof AuthedTournamentsIndexRoute
}
export interface FileRoutesByTo {
'/login': typeof LoginRoute
'/logout': typeof LogoutRoute
'/settings': typeof AuthedSettingsRoute
'/': typeof AuthedIndexRoute
'/admin/preview': typeof AuthedAdminPreviewRoute
'/profile/$playerId': typeof AuthedProfilePlayerIdRoute
'/teams/$teamId': typeof AuthedTeamsTeamIdRoute
'/tournaments/$tournamentId': typeof AuthedTournamentsTournamentIdRoute
'/admin': typeof AuthedAdminIndexRoute
'/tournaments': typeof AuthedTournamentsIndexRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/_authed': typeof AuthedRouteWithChildren
'/login': typeof LoginRoute
'/logout': typeof LogoutRoute
'/_authed/admin': typeof AuthedAdminRouteWithChildren
'/_authed/settings': typeof AuthedSettingsRoute
'/_authed/': typeof AuthedIndexRoute
'/_authed/admin/preview': typeof AuthedAdminPreviewRoute
'/_authed/profile/$playerId': typeof AuthedProfilePlayerIdRoute
'/_authed/teams/$teamId': typeof AuthedTeamsTeamIdRoute
'/_authed/tournaments/$tournamentId': typeof AuthedTournamentsTournamentIdRoute
'/_authed/admin/': typeof AuthedAdminIndexRoute
'/_authed/tournaments/': typeof AuthedTournamentsIndexRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths:
| '/login'
| '/logout'
| '/admin'
| '/settings'
| '/'
| '/admin/preview'
| '/profile/$playerId'
| '/teams/$teamId'
| '/tournaments/$tournamentId'
| '/admin/'
| '/tournaments'
fileRoutesByTo: FileRoutesByTo
to:
| '/login'
| '/logout'
| '/settings'
| '/'
| '/admin/preview'
| '/profile/$playerId'
| '/teams/$teamId'
| '/tournaments/$tournamentId'
| '/admin'
| '/tournaments'
id:
| '__root__'
| '/_authed'
| '/login'
| '/logout'
| '/_authed/admin'
| '/_authed/settings'
| '/_authed/'
| '/_authed/admin/preview'
| '/_authed/profile/$playerId'
| '/_authed/teams/$teamId'
| '/_authed/tournaments/$tournamentId'
| '/_authed/admin/'
| '/_authed/tournaments/'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
AuthedRoute: typeof AuthedRouteWithChildren
LoginRoute: typeof LoginRoute
LogoutRoute: typeof LogoutRoute
}
export interface FileServerRoutesByFullPath {
'/api/test': typeof ApiTestServerRoute
'/api/auth/$': typeof ApiAuthSplatServerRoute
'/api/events/$': typeof ApiEventsSplatServerRoute
}
export interface FileServerRoutesByTo {
'/api/test': typeof ApiTestServerRoute
'/api/auth/$': typeof ApiAuthSplatServerRoute
'/api/events/$': typeof ApiEventsSplatServerRoute
}
export interface FileServerRoutesById {
__root__: typeof rootServerRouteImport
'/api/test': typeof ApiTestServerRoute
'/api/auth/$': typeof ApiAuthSplatServerRoute
'/api/events/$': typeof ApiEventsSplatServerRoute
}
export interface FileServerRouteTypes {
fileServerRoutesByFullPath: FileServerRoutesByFullPath
fullPaths: '/api/test' | '/api/auth/$' | '/api/events/$'
fileServerRoutesByTo: FileServerRoutesByTo
to: '/api/test' | '/api/auth/$' | '/api/events/$'
id: '__root__' | '/api/test' | '/api/auth/$' | '/api/events/$'
fileServerRoutesById: FileServerRoutesById
}
export interface RootServerRouteChildren {
ApiTestServerRoute: typeof ApiTestServerRoute
ApiAuthSplatServerRoute: typeof ApiAuthSplatServerRoute
ApiEventsSplatServerRoute: typeof ApiEventsSplatServerRoute
}
declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/logout': {
id: '/logout'
path: '/logout'
fullPath: '/logout'
preLoaderRoute: typeof LogoutRouteImport
parentRoute: typeof rootRouteImport
}
'/login': {
id: '/login'
path: '/login'
fullPath: '/login'
preLoaderRoute: typeof LoginRouteImport
parentRoute: typeof rootRouteImport
}
'/_authed': {
id: '/_authed'
path: ''
fullPath: ''
preLoaderRoute: typeof AuthedRouteImport
parentRoute: typeof rootRouteImport
}
'/_authed/': {
id: '/_authed/'
path: '/'
fullPath: '/'
preLoaderRoute: typeof AuthedIndexRouteImport
parentRoute: typeof AuthedRoute
}
'/_authed/settings': {
id: '/_authed/settings'
path: '/settings'
fullPath: '/settings'
preLoaderRoute: typeof AuthedSettingsRouteImport
parentRoute: typeof AuthedRoute
}
'/_authed/admin': {
id: '/_authed/admin'
path: '/admin'
fullPath: '/admin'
preLoaderRoute: typeof AuthedAdminRouteImport
parentRoute: typeof AuthedRoute
}
'/_authed/tournaments/': {
id: '/_authed/tournaments/'
path: '/tournaments'
fullPath: '/tournaments'
preLoaderRoute: typeof AuthedTournamentsIndexRouteImport
parentRoute: typeof AuthedRoute
}
'/_authed/admin/': {
id: '/_authed/admin/'
path: '/'
fullPath: '/admin/'
preLoaderRoute: typeof AuthedAdminIndexRouteImport
parentRoute: typeof AuthedAdminRoute
}
'/_authed/tournaments/$tournamentId': {
id: '/_authed/tournaments/$tournamentId'
path: '/tournaments/$tournamentId'
fullPath: '/tournaments/$tournamentId'
preLoaderRoute: typeof AuthedTournamentsTournamentIdRouteImport
parentRoute: typeof AuthedRoute
}
'/_authed/teams/$teamId': {
id: '/_authed/teams/$teamId'
path: '/teams/$teamId'
fullPath: '/teams/$teamId'
preLoaderRoute: typeof AuthedTeamsTeamIdRouteImport
parentRoute: typeof AuthedRoute
}
'/_authed/profile/$playerId': {
id: '/_authed/profile/$playerId'
path: '/profile/$playerId'
fullPath: '/profile/$playerId'
preLoaderRoute: typeof AuthedProfilePlayerIdRouteImport
parentRoute: typeof AuthedRoute
}
'/_authed/admin/preview': {
id: '/_authed/admin/preview'
path: '/preview'
fullPath: '/admin/preview'
preLoaderRoute: typeof AuthedAdminPreviewRouteImport
parentRoute: typeof AuthedAdminRoute
}
}
}
declare module '@tanstack/react-start/server' {
interface ServerFileRoutesByPath {
'/api/test': {
id: '/api/test'
path: '/api/test'
fullPath: '/api/test'
preLoaderRoute: typeof ApiTestServerRouteImport
parentRoute: typeof rootServerRouteImport
}
'/api/events/$': {
id: '/api/events/$'
path: '/api/events/$'
fullPath: '/api/events/$'
preLoaderRoute: typeof ApiEventsSplatServerRouteImport
parentRoute: typeof rootServerRouteImport
}
'/api/auth/$': {
id: '/api/auth/$'
path: '/api/auth/$'
fullPath: '/api/auth/$'
preLoaderRoute: typeof ApiAuthSplatServerRouteImport
parentRoute: typeof rootServerRouteImport
}
}
}
interface AuthedAdminRouteChildren {
AuthedAdminPreviewRoute: typeof AuthedAdminPreviewRoute
AuthedAdminIndexRoute: typeof AuthedAdminIndexRoute
}
const AuthedAdminRouteChildren: AuthedAdminRouteChildren = {
AuthedAdminPreviewRoute: AuthedAdminPreviewRoute,
AuthedAdminIndexRoute: AuthedAdminIndexRoute,
}
const AuthedAdminRouteWithChildren = AuthedAdminRoute._addFileChildren(
AuthedAdminRouteChildren,
)
interface AuthedRouteChildren {
AuthedAdminRoute: typeof AuthedAdminRouteWithChildren
AuthedSettingsRoute: typeof AuthedSettingsRoute
AuthedIndexRoute: typeof AuthedIndexRoute
AuthedProfilePlayerIdRoute: typeof AuthedProfilePlayerIdRoute
AuthedTeamsTeamIdRoute: typeof AuthedTeamsTeamIdRoute
AuthedTournamentsTournamentIdRoute: typeof AuthedTournamentsTournamentIdRoute
AuthedTournamentsIndexRoute: typeof AuthedTournamentsIndexRoute
}
const AuthedRouteChildren: AuthedRouteChildren = {
AuthedAdminRoute: AuthedAdminRouteWithChildren,
AuthedSettingsRoute: AuthedSettingsRoute,
AuthedIndexRoute: AuthedIndexRoute,
AuthedProfilePlayerIdRoute: AuthedProfilePlayerIdRoute,
AuthedTeamsTeamIdRoute: AuthedTeamsTeamIdRoute,
AuthedTournamentsTournamentIdRoute: AuthedTournamentsTournamentIdRoute,
AuthedTournamentsIndexRoute: AuthedTournamentsIndexRoute,
}
const AuthedRouteWithChildren =
AuthedRoute._addFileChildren(AuthedRouteChildren)
const rootRouteChildren: RootRouteChildren = {
AuthedRoute: AuthedRouteWithChildren,
LoginRoute: LoginRoute,
LogoutRoute: LogoutRoute,
}
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)
._addFileTypes<FileRouteTypes>()
const rootServerRouteChildren: RootServerRouteChildren = {
ApiTestServerRoute: ApiTestServerRoute,
ApiAuthSplatServerRoute: ApiAuthSplatServerRoute,
ApiEventsSplatServerRoute: ApiEventsSplatServerRoute,
}
export const serverRouteTree = rootServerRouteImport
._addFileChildren(rootServerRouteChildren)
._addFileTypes<FileServerRouteTypes>()

37
src/app/router.tsx Normal file
View File

@@ -0,0 +1,37 @@
import { QueryClient } from '@tanstack/react-query'
import { createRouter as createTanStackRouter } from '@tanstack/react-router'
import { routerWithQueryClient } from '@tanstack/react-router-with-query'
import { routeTree } from './routeTree.gen'
import { DefaultCatchBoundary } from '../components/DefaultCatchBoundary'
import { defaultHeaderConfig } from '@/features/core/hooks/use-header-config'
export function createRouter() {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // 60 seconds
gcTime: 5 * 60 * 1000, // 5 minutes
refetchOnWindowFocus: false,
refetchOnReconnect: 'always',
retry: 3,
},
},
})
return routerWithQueryClient(
createTanStackRouter({
routeTree,
context: { queryClient, auth: undefined!, header: defaultHeaderConfig, refresh: { toRefresh: [] } },
defaultPreload: 'intent',
defaultErrorComponent: DefaultCatchBoundary,
scrollRestoration: true,
}),
queryClient,
)
}
declare module '@tanstack/react-router' {
interface Register {
router: ReturnType<typeof createRouter>
}
}

107
src/app/routes/__root.tsx Normal file
View File

@@ -0,0 +1,107 @@
import '@mantine/core/styles.css';
import '@mantine/dates/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 '@/components/providers'
import { ColorSchemeScript, mantineHtmlProps } from '@mantine/core';
import { HeaderConfig } from '@/features/core/types/header-config';
export const Route = createRootRouteWithContext<{
queryClient: QueryClient,
auth: AuthContextType,
header: HeaderConfig,
refresh: { toRefresh: string[] }
}>()({
head: () => ({
meta: [
{
charSet: 'utf-8'
},
{
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: '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' },
],
}),
errorComponent: (props) => {
return (
<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
};
}
})
function RootComponent() {
React.useEffect(() => {
ensureSuperTokensFrontend()
}, [])
return (
<RootDocument>
<Providers>
<Outlet />
</Providers>
</RootDocument>
)
}
function RootDocument({ children }: { children: React.ReactNode }) {
return (
<html {...mantineHtmlProps} style={{ overflowX: 'hidden', overflowY: 'hidden', position: 'fixed', width: '100%' }}>
<head>
<HeadContent />
<ColorSchemeScript />
<link rel="stylesheet" href="/styles.css" />
</head>
<body style={{ overflowX: 'hidden', overflowY: 'hidden', position: 'fixed', width: '100%' }}>
<div className='app'>
{children}
</div>
<Scripts />
</body>
</html>
)
}

View File

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

View File

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

View File

@@ -0,0 +1,22 @@
import { createFileRoute } from "@tanstack/react-router"
import { Title } from "@mantine/core";
import Page from "@/components/page";
import { playerQueries } from "@/features/players/queries";
import { useQuery } from "@tanstack/react-query";
import PlayerList from "@/features/players/components/player-list";
export const Route = createFileRoute("/_authed/admin/")({
loader: async ({ context }) => {
const { queryClient } = context;
await queryClient.ensureQueryData(playerQueries.list())
},
component: RouteComponent,
})
function RouteComponent() {
const { data: players, isLoading } = useQuery(playerQueries.list());
return <Page>
<Title order={2} mb='md'>Players</Title>
<PlayerList players={players!} loading={isLoading} />
</Page>
}

View File

@@ -0,0 +1,10 @@
import { PreviewBracketPage } from '@/features/bracket/components/bracket-page'
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/_authed/admin/preview')({
component: RouteComponent,
})
function RouteComponent() {
return <PreviewBracketPage />
}

View File

@@ -0,0 +1,29 @@
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());
},
});
function Home() {
return (
<Page noPadding>
<Box h='60vh' p="md">
<Text m='16vh' fw={500}>Some Content Here</Text>
</Box>
<Box>
<Text pl='md'>Quick Links</Text>
<Divider />
<ListLink label="All Tournaments" to="/tournaments" Icon={TrophyIcon} />
</Box>
</Page>
);
}

View File

@@ -0,0 +1,28 @@
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";
export const Route = createFileRoute("/_authed/profile/$playerId")({
beforeLoad: async ({ params, context }) => {
const { queryClient } = context;
const player = await queryClient.ensureQueryData(playerQueries.details(params.playerId))
if (!player) throw redirect({ to: '/' });
return {
player
}
},
loader: ({ params }) => ({
header: {
collapsed: true,
withBackButton: true
},
refresh: {
toRefresh: [playerQueries.details(params.playerId).queryKey],
}
}),
component: () => {
const { player } = Route.useRouteContext();
return <Page><Profile player={player} /></Page>
},
})

View File

@@ -0,0 +1,34 @@
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 Page from "@/components/page"
export const Route = createFileRoute("/_authed/settings")({
loader: () => ({
header: {
title: 'Settings',
withBackButton: true,
},
}),
component: RouteComponent,
})
function RouteComponent() {
return <Page noPadding>
<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}
/>
</Page>
}

View File

@@ -0,0 +1,26 @@
import Page from "@/components/page";
import TeamProfile from "@/features/teams/components/team-profile";
import { teamQueries } from "@/features/teams/queries";
import { redirect, createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/_authed/teams/$teamId")({
beforeLoad: async ({ params, context }) => {
const { queryClient } = context;
const team = await queryClient.ensureQueryData(teamQueries.details(params.teamId))
if (!team) throw redirect({ to: '/' });
return { team }
},
loader: ({ params }) => ({
header: {
collapsed: true,
withBackButton: true
},
refresh: {
toRefresh: [teamQueries.details(params.teamId).queryKey],
}
}),
component: () => {
const { team } = Route.useRouteContext();
return <Page><TeamProfile team={team} /></Page>
},
})

View File

@@ -0,0 +1,67 @@
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, Button } from '@mantine/core';
import { useSheet } from '@/hooks/use-sheet';
import Sheet from '@/components/sheet/sheet';
import { Tournament } from '@/features/tournaments/types';
import { UsersIcon } from '@phosphor-icons/react';
import ListButton from '@/components/list-button';
import TeamList from '@/features/teams/components/team-list';
export const Route = createFileRoute('/_authed/tournaments/$tournamentId')({
beforeLoad: async ({ context, params }) => {
const { queryClient } = context;
await queryClient.ensureQueryData(tournamentQueries.details(params.tournamentId))
},
loader: ({ params }) => ({
header: {
collapsed: true,
withBackButton: true
},
refresh: {
toRefresh: tournamentQueries.details(params.tournamentId).queryKey,
}
}),
component: RouteComponent,
})
function RouteComponent() {
const { data: tournament } = useQuery(tournamentQueries.details(Route.useParams().tournamentId));
const sheet = useSheet()
return <Page noPadding>
<Box mt='xl' p='md'>
<h3 style={{ marginTop: 0 }}>
{tournament?.name}
</h3>
<Button onClick={() => sheet.open()}>
View Teams
</Button>
</Box>
<ListButton
label='Teams'
onClick={() => sheet.open()}
Icon={UsersIcon}
/>
<Sheet
{...sheet.props}
title='Teams'
>
<TeamDrawer tournament={tournament!} />
</Sheet>
</Page>
}
const TeamDrawer = ({ tournament }: { tournament: Tournament }) => {
return (
<TeamList teams={tournament?.teams!} />
);
}

View File

@@ -0,0 +1,54 @@
import Page from '@/components/page'
import { Button, 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 { useAuth } from '@/contexts/auth-context'
import { useSheet } from '@/hooks/use-sheet'
import Sheet from '@/components/sheet/sheet'
import CreateTournament from '@/features/admin/components/create-tournament'
import { PlusIcon } from '@phosphor-icons/react'
export const Route = createFileRoute('/_authed/tournaments/')({
beforeLoad: async ({ context }) => {
const { queryClient } = context;
await queryClient.ensureQueryData(tournamentQueries.list())
},
loader: () => ({
header: {
withBackButton: true,
title: 'Tournaments',
},
refresh: {
toRefresh: tournamentQueries.list().queryKey,
}
}),
component: RouteComponent,
})
function RouteComponent() {
const { data: tournaments } = useQuery(tournamentQueries.list());
const { roles } = useAuth();
const sheet = useSheet();
return <Page>
<Stack>
{
roles?.includes("Admin") ? (
<>
<Button leftSection={<PlusIcon />} variant='subtle' onClick={sheet.open}>Create Tournament</Button>
<Sheet {...sheet.props} title='Create Tournament'>
<CreateTournament close={sheet.close} />
</Sheet>
</>
) : null
}
{
tournaments?.map((tournament: any) => (
<TournamentCard key={tournament.id} tournament={tournament} />
))
}
</Stack>
</Page>
}

View File

@@ -0,0 +1,19 @@
// API file that handles all supertokens auth routes
import { createServerFileRoute } from '@tanstack/react-start/server';
import { handleAuthAPIRequest } from 'supertokens-node/custom'
import { ensureSuperTokensBackend } from '@/lib/supertokens/server'
ensureSuperTokensBackend();
// forwards all supertokens api methods to our API
const superTokensHandler = handleAuthAPIRequest();
const handleRequest = async ({ request }: {request: Request}) => superTokensHandler(request);
export const ServerRoute = createServerFileRoute('/api/auth/$').methods({
GET: handleRequest,
POST: handleRequest,
PUT: handleRequest,
DELETE: handleRequest,
PATCH: handleRequest,
OPTIONS: handleRequest,
HEAD: handleRequest,
})

View File

@@ -0,0 +1,67 @@
import { createServerFileRoute } from "@tanstack/react-start/server";
import { serverEvents, type ServerEvent } from "@/lib/events/emitter";
import { logger } from "@/lib/logger";
import { superTokensRequestMiddleware } from "@/utils/supertokens";
export const ServerRoute = createServerFileRoute("/api/events/$").middleware([superTokensRequestMiddleware]).methods({
GET: ({ request, context }) => {
logger.info('ServerEvents | New connection', (context as any)?.userAuthId);
const stream = new ReadableStream({
start(controller) {
// Send initial connection messages
const connectMessage = `data: ${JSON.stringify({ type: "connected" })}\n\n`;
controller.enqueue(new TextEncoder().encode(connectMessage));
// Listen for events and broadcast to all connections
const handleEvent = (event: ServerEvent) => {
logger.info('ServerEvents | Event received', event);
const message = `data: ${JSON.stringify(event)}\n\n`;
try {
controller.enqueue(new TextEncoder().encode(message));
} catch (error) {
logger.error("ServerEvents | Error sending SSE message", error);
}
};
serverEvents.on("test", handleEvent);
// Keep alive ping every 30 seconds
const pingInterval = setInterval(() => {
try {
const pingMessage = `data: ${JSON.stringify({ type: "ping" })}\n\n`;
controller.enqueue(new TextEncoder().encode(pingMessage));
} catch (e) {
clearInterval(pingInterval);
controller.close();
}
}, 30000);
const cleanup = () => {
serverEvents.off("test", handleEvent);
clearInterval(pingInterval);
try {
logger.info('ServerEvents | Closing connection', (context as any)?.userAuthId);
controller.close();
} catch (e) {
logger.error('ServerEvents | Error closing controller', e);
}
};
request.signal?.addEventListener("abort", cleanup);
return cleanup;
},
});
return new Response(stream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Cache-Control",
},
});
},
});

View File

@@ -0,0 +1,9 @@
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!')
},
})

27
src/app/routes/login.tsx Normal file
View File

@@ -0,0 +1,27 @@
import LoginLayout from "@/features/login/components/layout";
import LoginFlow from "@/features/login/components/login-flow";
import { redirect, createFileRoute } from "@tanstack/react-router";
import z from "zod";
const loginSearchSchema = z.object({
stage: z.enum(['code', 'name']).optional(),
number: z.string().optional(),
callback: z.string().optional()
});
export const Route = createFileRoute("/login")({
validateSearch: loginSearchSchema,
beforeLoad: async ({ context }) => {
if (context.auth?.user) {
throw redirect({ to: '/' })
}
},
component: () => {
return (
<LoginLayout>
<LoginFlow />
</LoginLayout>
)
}
})

14
src/app/routes/logout.tsx Normal file
View File

@@ -0,0 +1,14 @@
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'
export const Route = createFileRoute('/logout')({
preload: false,
loader: async ({ context }) => {
await context.queryClient.setQueryData(authQueryConfig.queryKey, defaultAuthData);
await signOut();
throw redirect({ to: '/login' });
},
pendingComponent: () => <LoadingOverlay visible />
})

2
src/app/tanstack-start.d.ts vendored Normal file
View File

@@ -0,0 +1,2 @@
/// <reference types="vite/client" />
import '../../.tanstack-start/server-routes/routeTree.gen'