diff --git a/src/app/routes/_authed/admin/preview.tsx b/src/app/routes/_authed/admin/preview.tsx
index 076fb33..77cb00f 100644
--- a/src/app/routes/_authed/admin/preview.tsx
+++ b/src/app/routes/_authed/admin/preview.tsx
@@ -1,4 +1,3 @@
-import { PreviewBracket } from "@/features/bracket/components/preview";
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/_authed/admin/preview")({
@@ -13,5 +12,5 @@ export const Route = createFileRoute("/_authed/admin/preview")({
});
function RouteComponent() {
- return ;
+ return
Preview
;
}
diff --git a/src/app/routes/_authed/admin/tournaments/run.$id.tsx b/src/app/routes/_authed/admin/tournaments/run.$id.tsx
index dbb007c..5a864b4 100644
--- a/src/app/routes/_authed/admin/tournaments/run.$id.tsx
+++ b/src/app/routes/_authed/admin/tournaments/run.$id.tsx
@@ -1,7 +1,6 @@
import { createFileRoute, redirect } from '@tanstack/react-router'
import { tournamentQueries } from '@/features/tournaments/queries'
import { ensureServerQueryData } from '@/lib/tanstack-query/utils/ensure'
-import RunTournament from '@/features/tournaments/components/run-tournament'
export const Route = createFileRoute('/_authed/admin/tournaments/run/$id')({
beforeLoad: async ({ context, params }) => {
@@ -27,5 +26,5 @@ export const Route = createFileRoute('/_authed/admin/tournaments/run/$id')({
function RouteComponent() {
const { id } = Route.useParams()
- return
+ return Run tournament
}
diff --git a/src/app/routes/api/auth.$.ts b/src/app/routes/api/auth.$.ts
index cb75d5a..6d7eff7 100644
--- a/src/app/routes/api/auth.$.ts
+++ b/src/app/routes/api/auth.$.ts
@@ -9,7 +9,7 @@ ensureSuperTokensBackend();
const superTokensHandler = handleAuthAPIRequest();
const handleRequest = async ({ request }: {request: Request}) => {
console.log("=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=");
- console.log("Handling auth request:", request);
+ console.log("Handling auth request:", request.method, request.url);
return superTokensHandler(request);
};
export const ServerRoute = createServerFileRoute('/api/auth/$').methods({
diff --git a/src/features/bracket/components/bracket-round.tsx b/src/features/_bracket/components/bracket-round.tsx
similarity index 100%
rename from src/features/bracket/components/bracket-round.tsx
rename to src/features/_bracket/components/bracket-round.tsx
diff --git a/src/features/bracket/components/bracket-view.tsx b/src/features/_bracket/components/bracket-view.tsx
similarity index 100%
rename from src/features/bracket/components/bracket-view.tsx
rename to src/features/_bracket/components/bracket-view.tsx
diff --git a/src/features/bracket/components/bracket.tsx b/src/features/_bracket/components/bracket.tsx
similarity index 96%
rename from src/features/bracket/components/bracket.tsx
rename to src/features/_bracket/components/bracket.tsx
index aab4869..af7db61 100644
--- a/src/features/bracket/components/bracket.tsx
+++ b/src/features/_bracket/components/bracket.tsx
@@ -1,8 +1,8 @@
import { ScrollArea, Text } from "@mantine/core";
import BracketView from "./bracket-view";
-import { Match } from "../types";
import useAppShellHeight from "@/hooks/use-appshell-height";
import { BracketMaps } from "../utils/bracket-maps";
+import { Match } from "@/features/matches/types";
interface BracketProps {
winners: Match[][];
diff --git a/src/features/_bracket/components/match-card.tsx b/src/features/_bracket/components/match-card.tsx
new file mode 100644
index 0000000..1bde010
--- /dev/null
+++ b/src/features/_bracket/components/match-card.tsx
@@ -0,0 +1,72 @@
+import { ActionIcon, Card, Text } from '@mantine/core';
+import { PlayIcon } from '@phosphor-icons/react';
+import React, { useCallback, useMemo } from 'react';
+import { MatchSlot } from './match-slot';
+import { Match } from '../types';
+
+interface MatchCardProps {
+ match: Match;
+ getParentMatchOrder: (parentLid: number) => number | string;
+ onAnnounce?: (teamOne: any, teamTwo: any) => void;
+}
+
+export const MatchCard: React.FC = ({
+ match,
+ getParentMatchOrder,
+ onAnnounce
+}) => {
+
+ const showAnnounce = useMemo(() =>
+ onAnnounce && match.home.team && match.away.team,
+ [onAnnounce, match.home.team, match.away.team]);
+
+ const handleAnnounce = useCallback(() =>
+ onAnnounce?.(match.home.team, match.away.team), [match.home.team, match.away.team]);
+
+ return (
+
+
+
+
+
+
+
+
+
+ {match.reset && (
+
+ * If necessary
+
+ )}
+
+ {showAnnounce && (
+
+
+
+ )}
+
+ );
+};
\ No newline at end of file
diff --git a/src/features/_bracket/components/match-slot.tsx b/src/features/_bracket/components/match-slot.tsx
new file mode 100644
index 0000000..7babadc
--- /dev/null
+++ b/src/features/_bracket/components/match-slot.tsx
@@ -0,0 +1,51 @@
+import { Flex, Text } from "@mantine/core";
+import React from "react";
+import { SeedBadge } from "./seed-badge";
+
+interface MatchSlotProps {
+ slot: any;
+ getParentMatchOrder: (parentLid: number) => number | string;
+}
+
+export const MatchSlot: React.FC = ({
+ slot,
+ getParentMatchOrder,
+}) => {
+ const renderSlotContent = () => {
+ if (slot?.seed) {
+ return slot.team ? (
+ {slot.team.name}
+ ) : (
+
+ Team {slot.seed}
+
+ );
+ }
+
+ if (slot?.parent_lid !== null && slot?.parent_lid !== undefined) {
+ return (
+
+ {slot.loser ? "Loser" : "Winner"} of Match{" "}
+ {getParentMatchOrder(slot.parent_lid)}
+
+ );
+ }
+
+ if (slot) {
+ return (
+
+ TBD
+
+ );
+ }
+
+ return null;
+ };
+
+ return (
+
+ {slot?.seed && }
+ {renderSlotContent()}
+
+ );
+};
diff --git a/src/features/bracket/components/preview.tsx b/src/features/_bracket/components/preview.tsx
similarity index 100%
rename from src/features/bracket/components/preview.tsx
rename to src/features/_bracket/components/preview.tsx
diff --git a/src/features/_bracket/components/seed-badge.tsx b/src/features/_bracket/components/seed-badge.tsx
new file mode 100644
index 0000000..52eb316
--- /dev/null
+++ b/src/features/_bracket/components/seed-badge.tsx
@@ -0,0 +1,29 @@
+import { Text } from "@mantine/core";
+import React from "react";
+
+interface SeedBadgeProps {
+ seed: number;
+}
+
+export const SeedBadge: React.FC = ({ seed }) => {
+ return (
+
+ {seed}
+
+ );
+};
diff --git a/src/features/_bracket/components/seed-list.tsx b/src/features/_bracket/components/seed-list.tsx
new file mode 100644
index 0000000..89744e3
--- /dev/null
+++ b/src/features/_bracket/components/seed-list.tsx
@@ -0,0 +1,48 @@
+import { Flex, Text, Select, Card } from "@mantine/core";
+
+interface Team {
+ id: string;
+ name: string;
+}
+
+interface SeedListProps {
+ teams: Team[];
+ onSeedChange: (currentIndex: number, newIndex: number) => void;
+}
+
+export function SeedList({ teams, onSeedChange }: SeedListProps) {
+ const seedOptions = teams.map((_, index) => ({
+ value: index.toString(),
+ label: `Seed ${index + 1}`,
+ }));
+
+ return (
+
+ {teams.map((team, index) => (
+
+
+
+
+
+
+ ))}
+
+ );
+}
diff --git a/src/features/_bracket/components/styles.module.css b/src/features/_bracket/components/styles.module.css
new file mode 100644
index 0000000..21a150c
--- /dev/null
+++ b/src/features/_bracket/components/styles.module.css
@@ -0,0 +1,9 @@
+.bracket-container::-webkit-scrollbar {
+ display: none;
+}
+
+@media (min-width: 768px) {
+ .bracket-container {
+ box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.05);
+ }
+}
\ No newline at end of file
diff --git a/src/features/_bracket/queries.ts b/src/features/_bracket/queries.ts
new file mode 100644
index 0000000..037690b
--- /dev/null
+++ b/src/features/_bracket/queries.ts
@@ -0,0 +1,18 @@
+import { queryOptions } from "@tanstack/react-query";
+import { previewBracket } from "./server";
+import { useServerSuspenseQuery } from "@/lib/tanstack-query/hooks";
+import { BracketData } from "./types";
+
+const bracketKeys = {
+ preview: (teams: number) => ["bracket", "preview", teams] as const,
+};
+
+export const bracketQueries = {
+ preview: (teams: number) => ({
+ queryKey: bracketKeys.preview(teams),
+ queryFn: () => previewBracket({ data: teams }),
+ }),
+};
+
+export const useBracketPreview = (teams: number) =>
+ useServerSuspenseQuery(bracketQueries.preview(teams));
diff --git a/src/features/_bracket/server.ts b/src/features/_bracket/server.ts
new file mode 100644
index 0000000..7e97c96
--- /dev/null
+++ b/src/features/_bracket/server.ts
@@ -0,0 +1,21 @@
+import { superTokensFunctionMiddleware } from "@/utils/supertokens";
+import { createServerFn } from "@tanstack/react-start";
+import { z } from "zod";
+import { Logger } from "@/lib/logger";
+import brackets from "./utils";
+import { BracketData } from "./types";
+import { toServerResult } from "@/lib/tanstack-query/utils/to-server-result";
+
+const logger = new Logger("Bracket Generation");
+
+export const previewBracket = createServerFn()
+ .validator(z.number())
+ .middleware([superTokensFunctionMiddleware])
+ .handler(async ({ data: teams }) =>
+ toServerResult(async () => {
+ logger.info("Generating bracket", teams);
+ if (!Object.keys(brackets).includes(teams.toString()))
+ throw Error("Bracket not available");
+ return brackets[teams as keyof typeof brackets] as BracketData;
+ })
+ );
diff --git a/src/features/_bracket/types.ts b/src/features/_bracket/types.ts
new file mode 100644
index 0000000..77f0418
--- /dev/null
+++ b/src/features/_bracket/types.ts
@@ -0,0 +1,20 @@
+/*export interface Slot {
+ seed?: number;
+ team?: any;
+}
+
+export interface Match {
+ lid: number;
+ round: number;
+ order: number | null;
+ type: string;
+ home: Slot;
+ away: Slot;
+ reset?: boolean;
+}*/
+import { Match } from "../matches/types";
+
+export interface BracketData {
+ winners: Match[][];
+ losers: Match[][];
+}
\ No newline at end of file
diff --git a/src/features/bracket/utils/bracket-maps.ts b/src/features/_bracket/utils/bracket-maps.ts
similarity index 100%
rename from src/features/bracket/utils/bracket-maps.ts
rename to src/features/_bracket/utils/bracket-maps.ts
diff --git a/src/features/_bracket/utils/index.ts b/src/features/_bracket/utils/index.ts
new file mode 100644
index 0000000..2543ed2
--- /dev/null
+++ b/src/features/_bracket/utils/index.ts
@@ -0,0 +1,24 @@
+/**
+ * Imports saved json dumps of bracket generation from a python script that I didn't prioritize converting to TS
+ */
+import b12 from "../../../../scripts/brackets/12.json";
+import b13 from "../../../../scripts/brackets/13.json";
+import b14 from "../../../../scripts/brackets/14.json";
+import b15 from "../../../../scripts/brackets/15.json";
+import b16 from "../../../../scripts/brackets/16.json";
+import b17 from "../../../../scripts/brackets/17.json";
+import b18 from "../../../../scripts/brackets/18.json";
+import b19 from "../../../../scripts/brackets/19.json";
+import b20 from "../../../../scripts/brackets/20.json";
+
+export default {
+ 12: b12,
+ 13: b13,
+ 14: b14,
+ 15: b15,
+ 16: b16,
+ 17: b17,
+ 18: b18,
+ 19: b19,
+ 20: b20,
+};
diff --git a/src/features/bracket/types.ts b/src/features/bracket/types.ts
index f70b6b9..77f0418 100644
--- a/src/features/bracket/types.ts
+++ b/src/features/bracket/types.ts
@@ -12,33 +12,9 @@ export interface Match {
away: Slot;
reset?: boolean;
}*/
+import { Match } from "../matches/types";
export interface BracketData {
winners: Match[][];
losers: Match[][];
-}
-
-
-export interface Match {
- id: number;
- order: number;
- lid: number;
- round: number;
- reset: boolean;
- home_cups?: number;
- away_cups?: number;
- ot_count?: number;
- start_time?: string;
- end_time?: string;
- bye?: boolean;
- home_from_lid?: number;
- away_from_lid?: number;
- home_from_loser?: boolean;
- away_from_loser?: boolean;
- is_losers_bracket?: boolean;
- tournament?: string;
- home?: string;
- away?: string;
- created?: Date;
- updated?: Date;
}
\ No newline at end of file
diff --git a/src/features/matches/types.ts b/src/features/matches/types.ts
index dd63688..4b4f0fe 100644
--- a/src/features/matches/types.ts
+++ b/src/features/matches/types.ts
@@ -16,10 +16,10 @@ export interface Match {
away_from_lid: number;
home_from_loser: boolean;
away_from_loser: boolean;
- is_losers_bracket: 'winners' | 'losers';
- tournament_id: string;
- home_id: string;
- away_id: string;
+ is_losers_bracket: boolean;
+ tournament: string;
+ home: string;
+ away: string;
created: string;
updated: string;
}
@@ -32,26 +32,18 @@ export const matchInputSchema = z.object({
home_cups: z.number().int().min(0).optional().default(0),
away_cups: z.number().int().min(0).optional().default(0),
ot_count: z.number().int().min(0).optional().default(0),
- start_time: z.iso.datetime("Invalid start time format").optional(),
- end_time: z.iso.datetime("Invalid end time format").optional(),
+ start_time: z.iso.datetime().optional(),
+ end_time: z.iso.datetime().optional(),
bye: z.boolean().optional().default(false),
home_from_lid: z.number().int().min(1).optional(),
away_from_lid: z.number().int().min(1).optional(),
home_from_loser: z.boolean().optional().default(false),
away_from_loser: z.boolean().optional().default(false),
is_losers_bracket: z.boolean().optional().default(false),
- tournament_id: z.string().min(1),
- home_id: z.string().min(1).optional(),
- away_id: z.string().min(1).optional(),
-}).refine(
- (data) => {
- if (data.start_time && data.end_time) {
- return new Date(data.start_time) < new Date(data.end_time);
- }
- return true;
- },
- { message: "End time must be after start time", path: ["end_time"] }
-);
+ tournament: z.string().min(1),
+ home: z.string().min(1).optional(),
+ away: z.string().min(1).optional(),
+});
export type MatchInput = z.infer;
export type MatchUpdateInput = Partial;
diff --git a/src/features/tournaments/components/run-tournament.tsx b/src/features/tournaments/components/run-tournament.tsx
deleted file mode 100644
index 3ef5c65..0000000
--- a/src/features/tournaments/components/run-tournament.tsx
+++ /dev/null
@@ -1,181 +0,0 @@
-import { useTournament } from '../queries'
-import { Box, Grid, NumberInput, Stack, Text, Title, Group, Flex, Divider, ScrollArea, Button } from '@mantine/core'
-import { useState, useMemo, useEffect } from 'react'
-import Avatar from '@/components/avatar'
-import { useBracketPreview } from '@/features/bracket/queries'
-import Bracket from '@/features/bracket/components/bracket'
-import { createBracketMaps, BracketMaps } from '@/features/bracket/utils/bracket-maps'
-import { Match, BracketData } from '@/features/bracket/types'
-
-interface RunTournamentProps {
- tournamentId: string
-}
-
-interface TeamWithSeed {
- id: string
- name: string
- seed: number
-}
-
-const RunTournament = ({ tournamentId }: RunTournamentProps) => {
- const { data: tournament } = useTournament(tournamentId)
- const teamCount = tournament?.teams?.length || 0
- const { data: bracketData, isLoading } = useBracketPreview(teamCount)
-
- const [teamSeeds, setTeamSeeds] = useState>(() => {
- if (!tournament?.teams) return {}
- return tournament.teams.reduce((acc, team, index) => {
- acc[team.id] = index + 1
- return acc
- }, {} as Record)
- })
-
- const [seededWinnersBracket, setSeededWinnersBracket] = useState([])
- const [seededLosersBracket, setSeededLosersBracket] = useState([])
- const [bracketMaps, setBracketMaps] = useState(null)
-
- const sortedTeams = useMemo(() => {
- if (!tournament?.teams) return []
-
- return tournament.teams
- .map(team => ({
- ...team,
- seed: teamSeeds[team.id] || 1,
- }))
- .sort((a, b) => a.seed - b.seed)
- }, [tournament?.teams, teamSeeds])
-
- const handleSeedChange = (teamId: string, newSeed: number) => {
- if (newSeed < 1 || !tournament?.teams) return
-
- setTeamSeeds(prev => {
- const currSeed = prev[teamId]
- const newSeeds = { ...prev }
-
- const otherTeams = tournament.teams!.filter(team => team.id !== teamId)
-
- if (newSeed !== currSeed) {
- const currTeam = otherTeams.find(team => prev[team.id] === newSeed)
-
- if (currTeam) {
- const affectedTeams = otherTeams.filter(team => {
- const teamSeed = prev[team.id]
- return newSeed < currSeed
- ? teamSeed >= newSeed && teamSeed < currSeed
- : teamSeed > currSeed && teamSeed <= newSeed
- })
-
- affectedTeams.forEach(team => {
- const teamSeed = prev[team.id]
- if (newSeed < currSeed) {
- newSeeds[team.id] = teamSeed + 1
- } else {
- newSeeds[team.id] = teamSeed - 1
- }
- })
- }
-
- newSeeds[teamId] = newSeed
- }
-
- return newSeeds
- })
- }
-
- useEffect(() => {
- if (!bracketData || !tournament?.teams || sortedTeams.length === 0) return
-
- const maps = createBracketMaps(bracketData)
- setBracketMaps(maps)
-
- const mapBracket = (bracket: Match[][]) => {
- return bracket.map((round) =>
- round.map((match) => {
- const mappedMatch = { ...match }
-
- if (match.home?.seed && match.home.seed > 0) {
- const team = sortedTeams.find(t => t.seed === match.home.seed)
- if (team) {
- mappedMatch.home = {
- ...match.home,
- team: team,
- }
- }
- }
-
- if (match.away?.seed && match.away.seed > 0) {
- const team = sortedTeams.find(t => t.seed === match.away.seed)
- if (team) {
- mappedMatch.away = {
- ...match.away,
- team: team,
- }
- }
- }
-
- return mappedMatch
- })
- )
- }
-
- setSeededWinnersBracket(mapBracket(bracketData.winners))
- setSeededLosersBracket(mapBracket(bracketData.losers))
- }, [bracketData, sortedTeams])
-
- if (!tournament) throw new Error('Tournament not found.')
-
- return (
-
-
-
- Team Seeds
- {sortedTeams.map((team) => (
- <>
-
-
-
- {team.name}
-
- handleSeedChange(team.id, Number(value) || 1)}
- min={1}
- max={tournament.teams?.length || 1}
- size="xs"
- w={50}
- styles={{ input: { textAlign: 'center' } }}
- step={-1}
- />
-
-
- >
- ))}
-
-
-
-
-
-
- Tournament Bracket
- {isLoading ? (
-
- Loading bracket...
-
- ) : (
-
- )}
-
-
-
- )
-}
-
-export default RunTournament
\ No newline at end of file