diff --git a/pb_migrations/1757211840_updated_matches.js b/pb_migrations/1757211840_updated_matches.js
new file mode 100644
index 0000000..73755aa
--- /dev/null
+++ b/pb_migrations/1757211840_updated_matches.js
@@ -0,0 +1,44 @@
+///
+migrate((app) => {
+ const collection = app.findCollectionByNameOrId("pbc_2541054544")
+
+ // add field
+ collection.fields.addAt(18, new Field({
+ "hidden": false,
+ "id": "number1705071305",
+ "max": null,
+ "min": null,
+ "name": "home_seed",
+ "onlyInt": false,
+ "presentable": false,
+ "required": false,
+ "system": false,
+ "type": "number"
+ }))
+
+ // add field
+ collection.fields.addAt(19, new Field({
+ "hidden": false,
+ "id": "number3588777624",
+ "max": null,
+ "min": null,
+ "name": "away_seed",
+ "onlyInt": false,
+ "presentable": false,
+ "required": false,
+ "system": false,
+ "type": "number"
+ }))
+
+ return app.save(collection)
+}, (app) => {
+ const collection = app.findCollectionByNameOrId("pbc_2541054544")
+
+ // remove field
+ collection.fields.removeById("number1705071305")
+
+ // remove field
+ collection.fields.removeById("number3588777624")
+
+ return app.save(collection)
+})
diff --git a/pb_migrations/1757211934_updated_tournaments.js b/pb_migrations/1757211934_updated_tournaments.js
new file mode 100644
index 0000000..a5d31c6
--- /dev/null
+++ b/pb_migrations/1757211934_updated_tournaments.js
@@ -0,0 +1,28 @@
+///
+migrate((app) => {
+ const collection = app.findCollectionByNameOrId("pbc_340646327")
+
+ // add field
+ collection.fields.addAt(10, new Field({
+ "cascadeDelete": false,
+ "collectionId": "pbc_2541054544",
+ "hidden": false,
+ "id": "relation103159226",
+ "maxSelect": 999,
+ "minSelect": 0,
+ "name": "matches",
+ "presentable": false,
+ "required": false,
+ "system": false,
+ "type": "relation"
+ }))
+
+ return app.save(collection)
+}, (app) => {
+ const collection = app.findCollectionByNameOrId("pbc_340646327")
+
+ // remove field
+ collection.fields.removeById("relation103159226")
+
+ return app.save(collection)
+})
diff --git a/src/app/routes/_authed/admin/preview.tsx b/src/app/routes/_authed/admin/preview.tsx
index 629824a..b13c1d2 100644
--- a/src/app/routes/_authed/admin/preview.tsx
+++ b/src/app/routes/_authed/admin/preview.tsx
@@ -1,4 +1,4 @@
-import BracketPreview from "@/features/bracket/components/preview";
+import BracketPreview from "@/features/admin/components/preview";
import { NumberInput } from "@mantine/core";
import { createFileRoute } from "@tanstack/react-router";
import { useState } from "react";
diff --git a/src/app/routes/_authed/admin/tournaments/run.$id.tsx b/src/app/routes/_authed/admin/tournaments/run.$id.tsx
index 5a864b4..d348674 100644
--- a/src/app/routes/_authed/admin/tournaments/run.$id.tsx
+++ b/src/app/routes/_authed/admin/tournaments/run.$id.tsx
@@ -1,6 +1,9 @@
-import { createFileRoute, redirect } from '@tanstack/react-router'
+import { createFileRoute, redirect, useRouter } from '@tanstack/react-router'
import { tournamentQueries } from '@/features/tournaments/queries'
import { ensureServerQueryData } from '@/lib/tanstack-query/utils/ensure'
+import SeedTournament from '@/features/tournaments/components/seed-tournament'
+import { Container, Alert, Text } from '@mantine/core'
+import { Info } from '@phosphor-icons/react'
export const Route = createFileRoute('/_authed/admin/tournaments/run/$id')({
beforeLoad: async ({ context, params }) => {
@@ -25,6 +28,30 @@ export const Route = createFileRoute('/_authed/admin/tournaments/run/$id')({
})
function RouteComponent() {
- const { id } = Route.useParams()
- return
Run tournament
+ const { tournament } = Route.useRouteContext()
+ const router = useRouter()
+
+ const handleSuccess = () => {
+ router.navigate({
+ to: '/admin/tournaments/$id',
+ params: { id: tournament.id }
+ })
+ }
+
+ console.log('Tournament:', tournament)
+
+ return (
+
+ {
+ tournament.matches?.length ?
+ Matches
+ : (
+ )
+ }
+
+ )
}
diff --git a/src/features/_bracket/components/bracket-round.tsx b/src/features/_bracket/components/bracket-round.tsx
deleted file mode 100644
index af770ac..0000000
--- a/src/features/_bracket/components/bracket-round.tsx
+++ /dev/null
@@ -1,48 +0,0 @@
-import { Flex, Text } from '@mantine/core';
-import React from 'react';
-import { MatchCard } from './match-card';
-import { Match } from '../types';
-
-interface BracketRoundProps {
- matches: Match[];
- roundIndex: number;
- getParentMatchOrder: (parentLid: number) => number | string;
- onAnnounce?: (teamOne: any, teamTwo: any) => void;
-}
-
-export const BracketRound: React.FC = ({
- matches,
- roundIndex,
- getParentMatchOrder,
- onAnnounce,
-}) => {
- const isBye = (type: string) => type?.toLowerCase() === 'bye';
-
- return (
-
- {matches.map((match, matchIndex) => {
- if (!match) return null;
- if (isBye(match.type)) return <>>; // for spacing
-
- return (
-
-
- {match.order}
-
-
-
- );
- })}
-
- );
-};
\ No newline at end of file
diff --git a/src/features/_bracket/components/bracket-view.tsx b/src/features/_bracket/components/bracket-view.tsx
deleted file mode 100644
index 2f337bb..0000000
--- a/src/features/_bracket/components/bracket-view.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-import { Flex } from '@mantine/core';
-import React, { useCallback } from 'react';
-import { BracketMaps } from '../utils/bracket-maps';
-import { BracketRound } from './bracket-round';
-import { Match } from '../types';
-
-interface BracketViewProps {
- bracket: Match[][];
- bracketMaps: BracketMaps;
- onAnnounce?: (teamOne: any, teamTwo: any) => void;
-}
-
-const BracketView: React.FC = ({
- bracket,
- bracketMaps,
- onAnnounce,
-}) => {
-
- const getParentMatchOrder = useCallback((parentLid: number): number | string => {
- const parentMatch = bracketMaps.matchByLid.get(parentLid);
- if (
- parentMatch &&
- parentMatch.order !== null &&
- parentMatch.order !== undefined
- ) {
- return parentMatch.order;
- }
- return `Match ${parentLid}`;
- }, [bracketMaps]);
-
- return (
-
- {bracket.map((round, roundIndex) => (
-
- ))}
-
- );
-};
-
-export default BracketView;
\ No newline at end of file
diff --git a/src/features/_bracket/components/bracket.tsx b/src/features/_bracket/components/bracket.tsx
deleted file mode 100644
index af7db61..0000000
--- a/src/features/_bracket/components/bracket.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-import { ScrollArea, Text } from "@mantine/core";
-import BracketView from "./bracket-view";
-import useAppShellHeight from "@/hooks/use-appshell-height";
-import { BracketMaps } from "../utils/bracket-maps";
-import { Match } from "@/features/matches/types";
-
-interface BracketProps {
- winners: Match[][];
- losers?: Match[][];
- bracketMaps: BracketMaps | null;
-}
-
-const Bracket: React.FC = ({ winners, losers, bracketMaps }) => {
- const height = useAppShellHeight();
-
- if (!bracketMaps) return Bracket not available.
;
-
- return (
-
-
-
- Winners Bracket
-
-
-
- {losers && (
-
-
- Losers Bracket
-
-
-
- )}
-
- );
-};
-
-export default Bracket;
diff --git a/src/features/_bracket/components/match-card.tsx b/src/features/_bracket/components/match-card.tsx
deleted file mode 100644
index 1bde010..0000000
--- a/src/features/_bracket/components/match-card.tsx
+++ /dev/null
@@ -1,72 +0,0 @@
-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
deleted file mode 100644
index 7babadc..0000000
--- a/src/features/_bracket/components/match-slot.tsx
+++ /dev/null
@@ -1,51 +0,0 @@
-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
deleted file mode 100644
index 3286dd0..0000000
--- a/src/features/_bracket/components/preview.tsx
+++ /dev/null
@@ -1,120 +0,0 @@
-import {
- Text,
- Container,
- Flex,
- NumberInput,
- Group,
- Loader,
-} from "@mantine/core";
-import { useEffect, useState } from "react";
-import { bracketQueries, useBracketPreview } from "../queries";
-import { useQuery } from "@tanstack/react-query";
-import { createBracketMaps, BracketMaps } from "../utils/bracket-maps";
-import { BracketData, Match } from "../types";
-import Bracket from "./bracket";
-import "./styles.module.css";
-
-interface PreviewTeam {
- id: string;
- name: string;
-}
-
-export const PreviewBracket: React.FC = () => {
- const [teamCount, setTeamCount] = useState(20);
- const { data, isLoading, error } = useBracketPreview(teamCount);
-
- const [teams, setTeams] = useState([]);
-
- useEffect(() => {
- setTeams(
- Array.from({ length: teamCount }, (_, i) => ({
- id: `team-${i + 1}`,
- name: `Team ${i + 1}`,
- }))
- );
- }, [teamCount]);
-
- const [seededWinnersBracket, setSeededWinnersBracket] = useState(
- []
- );
- const [seededLosersBracket, setSeededLosersBracket] = useState([]);
- const [bracketMaps, setBracketMaps] = useState(null);
-
- useEffect(() => {
- if (!data || teams.length === 0) return;
-
- const maps = createBracketMaps(data);
- 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 teamIndex = match.home.seed - 1;
- if (teams[teamIndex]) {
- mappedMatch.home = {
- ...match.home,
- team: teams[teamIndex],
- };
- }
- }
-
- if (match.away?.seed && match.away.seed > 0) {
- const teamIndex = match.away.seed - 1;
- if (teams[teamIndex]) {
- mappedMatch.away = {
- ...match.away,
- team: teams[teamIndex],
- };
- }
- }
-
- return mappedMatch;
- })
- );
- };
-
- const bracketData = data as BracketData;
- setSeededWinnersBracket(mapBracket(bracketData.winners));
- setSeededLosersBracket(mapBracket(bracketData.losers));
- }, [teams, data]);
-
- if (error) return Error loading bracket
;
-
- return (
-
-
-
-
- Teams:
-
- setTeamCount(Number(value) || 12)}
- min={12}
- max={20}
- size="sm"
- w={80}
- allowDecimal={false}
- clampBehavior="strict"
- />
-
-
-
- {isLoading ? (
-
-
-
- ) : (
-
- )}
-
-
- );
-};
diff --git a/src/features/_bracket/components/seed-badge.tsx b/src/features/_bracket/components/seed-badge.tsx
deleted file mode 100644
index 52eb316..0000000
--- a/src/features/_bracket/components/seed-badge.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-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
deleted file mode 100644
index 89744e3..0000000
--- a/src/features/_bracket/components/seed-list.tsx
+++ /dev/null
@@ -1,48 +0,0 @@
-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
deleted file mode 100644
index 21a150c..0000000
--- a/src/features/_bracket/components/styles.module.css
+++ /dev/null
@@ -1,9 +0,0 @@
-.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
deleted file mode 100644
index 037690b..0000000
--- a/src/features/_bracket/queries.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-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
deleted file mode 100644
index 7e97c96..0000000
--- a/src/features/_bracket/server.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-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
deleted file mode 100644
index 77f0418..0000000
--- a/src/features/_bracket/types.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-/*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
deleted file mode 100644
index a4db22f..0000000
--- a/src/features/_bracket/utils/bracket-maps.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-import { BracketData, Match } from "../types";
-
-export interface BracketMaps {
- matchByLid: Map;
- matchByOrder: Map;
- allMatches: Match[];
-}
-
-export function createBracketMaps(bracketData: BracketData): BracketMaps {
- const matchByLid = new Map();
- const matchByOrder = new Map();
- const allMatches: Match[] = [];
-
- [...bracketData.winners, ...bracketData.losers].forEach((round) => {
- round.forEach((match) => {
- matchByLid.set(match.lid, match);
-
- if (match.order !== null && match.order !== undefined) {
- matchByOrder.set(match.order, match);
- }
-
- allMatches.push(match);
- });
- });
-
- return {
- matchByLid,
- matchByOrder,
- allMatches,
- };
-}
-
-export function getMatchByLid(
- maps: BracketMaps,
- lid: number
-): Match | undefined {
- return maps.matchByLid.get(lid);
-}
-
-export function getMatchByOrder(
- maps: BracketMaps,
- order: number
-): Match | undefined {
- return maps.matchByOrder.get(order);
-}
diff --git a/src/features/_bracket/utils/index.ts b/src/features/_bracket/utils/index.ts
deleted file mode 100644
index 2543ed2..0000000
--- a/src/features/_bracket/utils/index.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-/**
- * 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/components/preview.tsx b/src/features/admin/components/preview.tsx
similarity index 66%
rename from src/features/bracket/components/preview.tsx
rename to src/features/admin/components/preview.tsx
index a4f4ac3..c2293a3 100644
--- a/src/features/bracket/components/preview.tsx
+++ b/src/features/admin/components/preview.tsx
@@ -1,5 +1,5 @@
-import { useBracketPreview } from "../queries";
-import BracketView from "./bracket-view";
+import { useBracketPreview } from "../../bracket/queries";
+import BracketView from "../../bracket/components/bracket-view";
interface BracketPreviewProps {
n: number;
diff --git a/src/features/matches/server.ts b/src/features/matches/server.ts
new file mode 100644
index 0000000..909cd4a
--- /dev/null
+++ b/src/features/matches/server.ts
@@ -0,0 +1,136 @@
+import { superTokensAdminFunctionMiddleware } from "@/utils/supertokens";
+import { createServerFn } from "@tanstack/react-start";
+import { pbAdmin } from "@/lib/pocketbase/client";
+import { logger } from "@/lib/logger";
+import { z } from "zod";
+import { toServerResult } from "@/lib/tanstack-query/utils/to-server-result";
+import brackets from "@/features/bracket/utils";
+import { MatchInput } from "@/features/matches/types";
+
+const orderedTeamsSchema = z.object({
+ tournamentId: z.string(),
+ orderedTeamIds: z.array(z.string()),
+});
+
+export const generateTournamentBracket = createServerFn()
+ .validator(orderedTeamsSchema)
+ .middleware([superTokensAdminFunctionMiddleware])
+ .handler(async ({ data: { tournamentId, orderedTeamIds } }) =>
+ toServerResult(async () => {
+ logger.info('Generating tournament bracket', { tournamentId, teamCount: orderedTeamIds.length });
+
+ // Get tournament with teams
+ const tournament = await pbAdmin.getTournament(tournamentId);
+ if (!tournament) {
+ throw new Error('Tournament not found');
+ }
+
+ // Check if tournament already has matches
+ if (tournament.matches && tournament.matches.length > 0) {
+ throw new Error('Tournament already has matches generated');
+ }
+
+ // Get bracket template based on team count
+ const teamCount = orderedTeamIds.length;
+ if (!Object.keys(brackets).includes(teamCount.toString())) {
+ throw new Error(`Bracket not available for ${teamCount} teams`);
+ }
+
+ const bracketTemplate = brackets[teamCount as keyof typeof brackets] as any;
+
+ // Create seed to team mapping (index + 1 = seed)
+ const seedToTeamId = new Map();
+ orderedTeamIds.forEach((teamId, index) => {
+ seedToTeamId.set(index + 1, teamId);
+ });
+
+ // Convert bracket template to match records
+ const matchInputs: MatchInput[] = [];
+
+ // Process winners bracket
+ bracketTemplate.winners.forEach((round: any[]) => {
+ round.forEach((match: any) => {
+ const matchInput: MatchInput = {
+ lid: match.lid,
+ round: match.round,
+ order: match.order || 0,
+ reset: match.reset || false,
+ bye: match.bye || false,
+ home_cups: 0,
+ away_cups: 0,
+ ot_count: 0,
+ home_from_lid: match.home_from_lid,
+ away_from_lid: match.away_from_lid,
+ home_from_loser: match.home_from_loser || false,
+ away_from_loser: match.away_from_loser || false,
+ is_losers_bracket: false,
+ tournament: tournamentId,
+ };
+
+ // Assign teams based on seeds
+ if (match.home_seed) {
+ const teamId = seedToTeamId.get(match.home_seed);
+ if (teamId) {
+ matchInput.home = teamId;
+ matchInput.home_seed = match.home_seed;
+ }
+ }
+
+ if (match.away_seed) {
+ const teamId = seedToTeamId.get(match.away_seed);
+ if (teamId) {
+ matchInput.away = teamId;
+ matchInput.away_seed = match.away_seed;
+ }
+ }
+
+ matchInputs.push(matchInput);
+ });
+ });
+
+ // Process losers bracket
+ bracketTemplate.losers.forEach((round: any[]) => {
+ round.forEach((match: any) => {
+ const matchInput: MatchInput = {
+ lid: match.lid,
+ round: match.round,
+ order: match.order || 0,
+ reset: match.reset || false,
+ bye: match.bye || false,
+ home_cups: 0,
+ away_cups: 0,
+ ot_count: 0,
+ home_from_lid: match.home_from_lid,
+ away_from_lid: match.away_from_lid,
+ home_from_loser: match.home_from_loser || false,
+ away_from_loser: match.away_from_loser || false,
+ is_losers_bracket: true,
+ tournament: tournamentId,
+ };
+
+ // Losers bracket matches don't start with teams
+ // Teams come from winners bracket losses
+
+ matchInputs.push(matchInput);
+ });
+ });
+
+ // Create all matches
+ const createdMatches = await pbAdmin.createMatches(matchInputs);
+
+ // Update tournament to include all match IDs in the matches relation
+ const matchIds = createdMatches.map(match => match.id);
+ await pbAdmin.updateTournamentMatches(tournamentId, matchIds);
+
+ logger.info('Tournament bracket generated', {
+ tournamentId,
+ matchCount: createdMatches.length
+ });
+
+ return {
+ tournament,
+ matchCount: createdMatches.length,
+ matches: createdMatches,
+ };
+ })
+ );
\ No newline at end of file
diff --git a/src/features/matches/types.ts b/src/features/matches/types.ts
index 39aa074..2174cc6 100644
--- a/src/features/matches/types.ts
+++ b/src/features/matches/types.ts
@@ -1,4 +1,6 @@
import { z } from "zod";
+import { TeamInfo } from "../teams/types";
+import { TournamentInfo } from "../tournaments/types";
/**
* class TMatchSlot(BaseModel):
@@ -50,9 +52,9 @@ export interface Match {
home_from_loser: boolean;
away_from_loser: boolean;
is_losers_bracket: boolean;
- tournament: string;
- home: string;
- away: string;
+ tournament: TournamentInfo;
+ home?: TeamInfo;
+ away?: TeamInfo;
created: string;
updated: string;
home_seed?: number;
@@ -78,7 +80,8 @@ export const matchInputSchema = z.object({
tournament: z.string().min(1),
home: z.string().min(1).optional(),
away: z.string().min(1).optional(),
- seed: z.number().int().min(1).optional(),
+ home_seed: z.number().int().min(1).optional(),
+ away_seed: z.number().int().min(1).optional(),
});
export type MatchInput = z.infer;
diff --git a/src/features/players/components/profile/index.tsx b/src/features/players/components/profile/index.tsx
index c6d5236..761c020 100644
--- a/src/features/players/components/profile/index.tsx
+++ b/src/features/players/components/profile/index.tsx
@@ -3,6 +3,7 @@ import Header from "./header";
import { Player } from "@/features/players/types";
import SwipeableTabs from "@/components/swipeable-tabs";
import { usePlayer } from "../../queries";
+import TeamList from "@/features/teams/components/team-list";
interface ProfileProps {
id: string;
@@ -13,24 +14,26 @@ const Profile = ({ id }: ProfileProps) => {
const tabs = [
{
label: "Overview",
- content: Stats/Badges will go here
+ content: Stats/Badges will go here,
},
{
label: "Matches",
- content: Matches feed will go here
+ content: Matches feed will go here,
},
{
label: "Teams",
- content: Teams will go here
- }
+ content: ,
+ },
];
- return <>
-
-
-
-
- >;
+ return (
+ <>
+
+
+
+
+ >
+ );
};
export default Profile;
diff --git a/src/features/players/types.ts b/src/features/players/types.ts
index 4ffdc80..0504b6b 100644
--- a/src/features/players/types.ts
+++ b/src/features/players/types.ts
@@ -1,6 +1,12 @@
-import { Team } from "@/features/teams/types";
+import { TeamInfo } from "@/features/teams/types";
import { z } from 'zod';
+export interface PlayerInfo {
+ id: string;
+ first_name?: string;
+ last_name?: string;
+}
+
export interface Player {
id: string;
auth_id?: string;
@@ -8,7 +14,7 @@ export interface Player {
last_name?: string;
created?: string;
updated?: string;
- teams?: Team[];
+ teams?: TeamInfo[];
}
export const playerInputSchema = z.object({
diff --git a/src/features/teams/components/team-list.tsx b/src/features/teams/components/team-list.tsx
index 81b3221..2ea15b3 100644
--- a/src/features/teams/components/team-list.tsx
+++ b/src/features/teams/components/team-list.tsx
@@ -1,12 +1,12 @@
import { List, ListItem, Skeleton, Stack, Text } from "@mantine/core";
import Avatar from "@/components/avatar";
-import { Team } from "@/features/teams/types";
+import { TeamInfo } from "@/features/teams/types";
import { useNavigate } from "@tanstack/react-router";
import { useCallback, useMemo } from "react";
import React from "react";
interface TeamListItemProps {
- team: Team;
+ team: TeamInfo;
}
const TeamListItem = React.memo(({ team }: TeamListItemProps) => {
const playerNames = useMemo(
@@ -29,7 +29,7 @@ const TeamListItem = React.memo(({ team }: TeamListItemProps) => {
});
interface TeamListProps {
- teams: Team[];
+ teams: TeamInfo[];
loading?: boolean;
}
diff --git a/src/features/teams/types.ts b/src/features/teams/types.ts
index 98fd725..e213303 100644
--- a/src/features/teams/types.ts
+++ b/src/features/teams/types.ts
@@ -1,6 +1,6 @@
-import { Player } from "@/features/players/types";
+import { PlayerInfo } from "@/features/players/types";
import { z } from "zod";
-import { Tournament } from "../tournaments/types";
+import { TournamentInfo } from "../tournaments/types";
export interface Team {
id: string;
@@ -18,8 +18,8 @@ export interface Team {
song_image_url: string;
created: string;
updated: string;
- players: Player[];
- tournaments: Tournament[];
+ players: PlayerInfo[];
+ tournaments: TournamentInfo[];
}
export interface TeamInfo {
@@ -27,6 +27,7 @@ export interface TeamInfo {
name: string;
primary_color: string;
accent_color: string;
+ players: PlayerInfo[];
}
export const teamInputSchema = z
diff --git a/src/features/tournaments/components/edit-enrolled-teams.tsx b/src/features/tournaments/components/edit-enrolled-teams.tsx
index 01150f0..0f857e2 100644
--- a/src/features/tournaments/components/edit-enrolled-teams.tsx
+++ b/src/features/tournaments/components/edit-enrolled-teams.tsx
@@ -12,14 +12,14 @@ import { useTournament, useUnenrolledTeams } from "../queries";
import useEnrollTeam from "../hooks/use-enroll-team";
import useUnenrollTeam from "../hooks/use-unenroll-team";
import Avatar from "@/components/avatar";
-import { Team } from "@/features/teams/types";
+import { Team, TeamInfo } from "@/features/teams/types";
interface EditEnrolledTeamsProps {
tournamentId: string;
}
interface TeamItemProps {
- team: Team;
+ team: TeamInfo;
onUnenroll: (teamId: string) => void;
disabled: boolean;
}
@@ -142,7 +142,7 @@ const EditEnrolledTeams = ({ tournamentId }: EditEnrolledTeamsProps) => {
) : (
- {enrolledTeams.map((team: Team) => (
+ {enrolledTeams.map((team: TeamInfo) => (
void;
+}
+
+const SeedTournament: React.FC = ({
+ tournamentId,
+ teams,
+ onSuccess,
+}) => {
+ const [orderedTeams, setOrderedTeams] = useState(teams);
+ const { data: bracketPreview } = useBracketPreview(teams.length);
+
+ const bracket: BracketData = useMemo(
+ () => ({
+ winners: bracketPreview.winners.map((round) =>
+ round.map((match) => ({
+ ...match,
+ away:
+ match.away_seed !== undefined
+ ? orderedTeams[match.away_seed - 1]
+ : undefined,
+ home:
+ match.home_seed !== undefined
+ ? orderedTeams[match.home_seed - 1]
+ : undefined,
+ }))
+ ),
+ losers: bracketPreview.losers
+ }),
+ [bracketPreview, orderedTeams]
+ );
+
+ const generateBracket = useServerMutation({
+ mutationFn: generateTournamentBracket,
+ successMessage: "Tournament bracket generated successfully!",
+ onSuccess: () => {
+ onSuccess?.();
+ },
+ });
+
+ const handleDragEnd = (result: any) => {
+ if (!result.destination) return;
+
+ const items = Array.from(orderedTeams);
+ const [reorderedItem] = items.splice(result.source.index, 1);
+ items.splice(result.destination.index, 0, reorderedItem);
+
+ setOrderedTeams(items);
+ };
+
+ const handleSeedChange = (teamId: string, newSeed: number) => {
+ if (newSeed < 1 || newSeed > orderedTeams.length) return;
+
+ const currentIndex = orderedTeams.findIndex((t) => t.id === teamId);
+ if (currentIndex === -1) return;
+
+ const targetIndex = newSeed - 1;
+ const items = Array.from(orderedTeams);
+ const [movedTeam] = items.splice(currentIndex, 1);
+ items.splice(targetIndex, 0, movedTeam);
+
+ setOrderedTeams(items);
+ };
+
+ const handleGenerateBracket = () => {
+ const orderedTeamIds = orderedTeams.map((team) => team.id);
+
+ generateBracket.mutate({
+ data: {
+ tournamentId,
+ orderedTeamIds,
+ },
+ });
+ };
+
+ return (
+
+
+
+
+
+
+
+ Team Seeding
+
+
+ {orderedTeams.length} teams
+
+
+
+
+
+
+
+ {(provided) => (
+
+ {orderedTeams.map((team, index) => (
+
+ {(provided) => (
+
+
+
+
+
+
+
+ handleSeedChange(team.id, Number(value) || 1)
+ }
+ min={1}
+ max={orderedTeams.length}
+ size="xs"
+ w={50}
+ styles={{
+ input: {
+ textAlign: "center",
+ fontWeight: 600,
+ height: 28,
+ },
+ }}
+ />
+
+
+
+
+ {team.name}
+
+
+
+ )}
+
+ ))}
+ {provided.placeholder}
+
+ )}
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default SeedTournament;
diff --git a/src/features/tournaments/components/tournament-list.tsx b/src/features/tournaments/components/tournament-list.tsx
index 2e2aa23..e7d6009 100644
--- a/src/features/tournaments/components/tournament-list.tsx
+++ b/src/features/tournaments/components/tournament-list.tsx
@@ -1,11 +1,11 @@
import { List, ListItem, Skeleton, Text } from "@mantine/core";
import { useNavigate } from "@tanstack/react-router";
import Avatar from "@/components/avatar";
-import { Tournament } from "../types";
+import { TournamentInfo } from "../types";
import { useCallback } from "react";
interface TournamentListProps {
- tournaments: Tournament[];
+ tournaments: TournamentInfo[];
loading?: boolean;
}
diff --git a/src/features/tournaments/types.ts b/src/features/tournaments/types.ts
index 1807bb5..08114c1 100644
--- a/src/features/tournaments/types.ts
+++ b/src/features/tournaments/types.ts
@@ -1,6 +1,16 @@
-import { Team } from "@/features/teams/types";
+import { TeamInfo } from "@/features/teams/types";
+import { Match } from "@/features/matches/types";
import { z } from "zod";
+export interface TournamentInfo {
+ id: string;
+ name: string;
+ location?: string;
+ start_time?: string;
+ end_time?: string;
+ logo?: string;
+}
+
export interface Tournament {
id: string;
name: string;
@@ -13,7 +23,8 @@ export interface Tournament {
end_time?: string;
created: string;
updated: string;
- teams?: Team[];
+ teams?: TeamInfo[];
+ matches?: Match[];
}
export const tournamentInputSchema = z.object({
diff --git a/src/lib/pocketbase/client.ts b/src/lib/pocketbase/client.ts
index dc3589f..15f7847 100644
--- a/src/lib/pocketbase/client.ts
+++ b/src/lib/pocketbase/client.ts
@@ -2,6 +2,7 @@ import PocketBase from "pocketbase";
import { createPlayersService } from "./services/players";
import { createTournamentsService } from "./services/tournaments";
import { createTeamsService } from "./services/teams";
+import { createMatchesService } from "./services/matches";
class PocketBaseAdminClient {
private pb: PocketBase;
@@ -29,6 +30,7 @@ class PocketBaseAdminClient {
Object.assign(this, createPlayersService(this.pb));
Object.assign(this, createTeamsService(this.pb));
Object.assign(this, createTournamentsService(this.pb));
+ Object.assign(this, createMatchesService(this.pb));
});
}
@@ -46,7 +48,8 @@ interface AdminClient
extends PocketBaseAdminClient,
ReturnType,
ReturnType,
- ReturnType {
+ ReturnType,
+ ReturnType {
authPromise: Promise;
}
diff --git a/src/lib/pocketbase/services/base.ts b/src/lib/pocketbase/services/base.ts
new file mode 100644
index 0000000..c97cc41
--- /dev/null
+++ b/src/lib/pocketbase/services/base.ts
@@ -0,0 +1,4 @@
+export interface DataFetchOptions {
+ includeRelations?: boolean;
+ expand?: string;
+}
\ No newline at end of file
diff --git a/src/lib/pocketbase/services/matches.ts b/src/lib/pocketbase/services/matches.ts
new file mode 100644
index 0000000..172b88b
--- /dev/null
+++ b/src/lib/pocketbase/services/matches.ts
@@ -0,0 +1,39 @@
+import { logger } from "@/lib/logger";
+import type { Match, MatchInput } from "@/features/matches/types";
+import type PocketBase from "pocketbase";
+
+export function createMatchesService(pb: PocketBase) {
+ return {
+ async createMatch(data: MatchInput): Promise {
+ logger.info("PocketBase | Creating match", data);
+ const result = await pb.collection("matches").create(data);
+ return result;
+ },
+
+ async createMatches(matches: MatchInput[]): Promise {
+ logger.info("PocketBase | Creating multiple matches", { count: matches.length });
+ const results = await Promise.all(
+ matches.map(match => pb.collection("matches").create(match))
+ );
+ return results;
+ },
+
+ async updateMatch(id: string, data: Partial): Promise {
+ logger.info("PocketBase | Updating match", { id, data });
+ const result = await pb.collection("matches").update(id, data);
+ return result;
+ },
+
+ async deleteMatchesByTournament(tournamentId: string): Promise {
+ logger.info("PocketBase | Deleting matches for tournament", tournamentId);
+ const matches = await pb.collection("matches").getFullList({
+ filter: `tournament = "${tournamentId}"`,
+ fields: "id",
+ });
+
+ await Promise.all(
+ matches.map(match => pb.collection("matches").delete(match.id))
+ );
+ },
+ };
+}
\ No newline at end of file
diff --git a/src/lib/pocketbase/services/players.ts b/src/lib/pocketbase/services/players.ts
index 79f28f6..690eda1 100644
--- a/src/lib/pocketbase/services/players.ts
+++ b/src/lib/pocketbase/services/players.ts
@@ -1,13 +1,28 @@
import type {
Player,
+ PlayerInfo,
PlayerInput,
PlayerUpdateInput,
} from "@/features/players/types";
-import { transformPlayer } from "@/lib/pocketbase/util/transform-types";
+import { transformPlayer, transformPlayerInfo } from "@/lib/pocketbase/util/transform-types";
import PocketBase from "pocketbase";
+import { DataFetchOptions } from "./base";
export function createPlayersService(pb: PocketBase) {
return {
+ async getPlayerInfo(id: string): Promise {
+ const result = await pb.collection("players").getOne(id, {
+ fields: "id,first_name,last_name"
+ });
+ return transformPlayerInfo(result);
+ },
+
+ async listPlayerInfos(): Promise {
+ const result = await pb.collection("players").getFullList({
+ fields: "id,first_name,last_name",
+ });
+ return result.map(transformPlayerInfo);
+ },
async getPlayerByAuthId(authId: string): Promise {
const result = await pb.collection("players").getList(1, 1, {
filter: `auth_id = "${authId}"`,
diff --git a/src/lib/pocketbase/services/teams.ts b/src/lib/pocketbase/services/teams.ts
index beeed6c..434af2a 100644
--- a/src/lib/pocketbase/services/teams.ts
+++ b/src/lib/pocketbase/services/teams.ts
@@ -1,10 +1,26 @@
import { logger } from "@/lib/logger";
import PocketBase from "pocketbase";
-import { transformTeam } from "@/lib/pocketbase/util/transform-types";
-import { Team } from "@/features/teams/types";
+import { transformTeam, transformTeamInfo } from "@/lib/pocketbase/util/transform-types";
+import { Team, TeamInfo } from "@/features/teams/types";
+import { DataFetchOptions } from "./base";
export function createTeamsService(pb: PocketBase) {
return {
+ async getTeamInfo(id: string): Promise {
+ logger.info("PocketBase | Getting team info", id);
+ const result = await pb.collection("teams").getOne(id, {
+ fields: "id,name,primary_color,accent_color"
+ });
+ return transformTeamInfo(result);
+ },
+
+ async listTeamInfos(): Promise {
+ logger.info("PocketBase | Listing team infos");
+ const result = await pb.collection("teams").getFullList({
+ fields: "id,name,primary_color,accent_color"
+ });
+ return result.map(transformTeamInfo);
+ },
async getTeam(id: string): Promise {
logger.info("PocketBase | Getting team", id);
const result = await pb.collection("teams").getOne(id, {
diff --git a/src/lib/pocketbase/services/tournaments.ts b/src/lib/pocketbase/services/tournaments.ts
index 4e18d84..db6585d 100644
--- a/src/lib/pocketbase/services/tournaments.ts
+++ b/src/lib/pocketbase/services/tournaments.ts
@@ -1,32 +1,32 @@
import { logger } from "@/lib/logger";
import type {
Tournament,
+ TournamentInfo,
TournamentInput,
TournamentUpdateInput,
} from "@/features/tournaments/types";
import type { Team } from "@/features/teams/types";
import PocketBase from "pocketbase";
-import { transformTournament } from "@/lib/pocketbase/util/transform-types";
+import { transformTournament, transformTournamentInfo } from "@/lib/pocketbase/util/transform-types";
import { transformTeam } from "@/lib/pocketbase/util/transform-types";
export function createTournamentsService(pb: PocketBase) {
return {
- async getTournament(id: string): Promise {
- logger.info("PocketBase | Getting tournament", id);
+ async getTournament(id: string): Promise {
const result = await pb.collection("tournaments").getOne(id, {
- expand: "teams, teams.players",
+ expand: "teams, teams.players, matches, matches.tournament",
});
return transformTournament(result);
},
- async listTournaments(): Promise {
+ async listTournaments(): Promise {
const result = await pb
.collection("tournaments")
- .getFullList({
- fields: "id,name,start_time,end_time,logo,created",
+ .getFullList({
+ fields: "id,name,location,start_time,end_time,logo",
sort: "-created",
});
- return result.map(transformTournament);
+ return result.map(transformTournamentInfo);
},
async createTournament(data: TournamentInput): Promise {
const result = await pb
@@ -79,6 +79,18 @@ export function createTournamentsService(pb: PocketBase) {
return transformTournament(result);
},
+ async updateTournamentMatches(
+ tournamentId: string,
+ matchIds: string[]
+ ): Promise {
+ logger.info("PocketBase | Updating tournament matches", { tournamentId, matchCount: matchIds.length });
+ const result = await pb
+ .collection("tournaments")
+ .update(tournamentId, {
+ matches: matchIds
+ });
+ return transformTournament(result);
+ },
async getUnenrolledTeams(tournamentId: string): Promise {
try {
logger.info(
diff --git a/src/lib/pocketbase/util/transform-types.ts b/src/lib/pocketbase/util/transform-types.ts
index 283a6bd..f71d1f0 100644
--- a/src/lib/pocketbase/util/transform-types.ts
+++ b/src/lib/pocketbase/util/transform-types.ts
@@ -1,18 +1,75 @@
-import { Player } from "@/features/players/types";
-import { Team } from "@/features/teams/types";
-import { Tournament } from "@/features/tournaments/types";
+import { Match } from "@/features/matches/types";
+import { Player, PlayerInfo } from "@/features/players/types";
+import { Team, TeamInfo } from "@/features/teams/types";
+import { Tournament, TournamentInfo } from "@/features/tournaments/types";
// pocketbase does this weird thing with relations where it puts them under a seperate "expand" field
// this file transforms raw pocketbase results to our types
+export function transformPlayerInfo(record: any): PlayerInfo {
+ return {
+ id: record.id,
+ first_name: record.first_name,
+ last_name: record.last_name,
+ };
+}
+
+export function transformTeamInfo(record: any): TeamInfo {
+ const players = record.expand?.players?.map(transformPlayerInfo) ?? [];
+ return {
+ id: record.id,
+ name: record.name,
+ primary_color: record.primary_color,
+ accent_color: record.accent_color,
+ players,
+ };
+}
+
+export const transformMatch = (record: any): Match => {
+ return {
+ id: record.id,
+ order: record.name,
+ lid: record.lid,
+ reset: record.reset,
+ round: record.round,
+ home_cups: record.home_cups,
+ away_cups: record.away_cups,
+ ot_count: record.ot_count,
+ start_time: record.start_time,
+ end_time: record.end_time,
+ bye: record.bye,
+ home_from_lid: record.home_from_lid,
+ away_from_lid: record.away_from_lid,
+ home_from_loser: record.home_from_loser,
+ away_from_loser: record.away_from_loser,
+ is_losers_bracket: record.is_losers_bracket,
+ tournament: transformTournamentInfo(record.expand?.tournament),
+ home: record.expand?.home ? transformTeamInfo(record.expand.home) : undefined,
+ away: record.expand?.away ? transformTeamInfo(record.expand.away) : undefined,
+ created: record.created,
+ updated: record.updated,
+ home_seed: record.home_seed,
+ away_seed: record.away_seed,
+ };
+}
+
+export const transformTournamentInfo = (record: any): TournamentInfo => {
+ return {
+ id: record.id,
+ name: record.name,
+ location: record.location,
+ start_time: record.start_time,
+ logo: record.logo,
+ };
+}
+
export function transformPlayer(record: any): Player {
- const sadf: string[] = [];
const teams =
record.expand?.teams
- ?.sort((a: Team, b: Team) =>
+ ?.sort((a: any, b: any) =>
new Date(a.created) < new Date(b.created) ? -1 : 0
)
- ?.map(transformTeam) ?? [];
+ ?.map(transformTeamInfo) ?? [];
return {
id: record.id!,
@@ -28,16 +85,16 @@ export function transformPlayer(record: any): Player {
export function transformTeam(record: any): Team {
const players =
record.expand?.players
- ?.sort((a: Player, b: Player) =>
+ ?.sort((a: any, b: any) =>
new Date(a.created!) < new Date(b.created!) ? -1 : 0
)
- ?.map(transformPlayer) ?? [];
+ ?.map(transformPlayerInfo) ?? [];
const tournaments =
record.expand?.tournaments
- ?.sort((a: Tournament, b: Tournament) =>
+ ?.sort((a: any, b: any) =>
new Date(a.created!) < new Date(b.created!) ? -1 : 0
)
- ?.map(transformTournament) ?? [];
+ ?.map(transformTournamentInfo) ?? [];
return {
id: record.id,
@@ -63,11 +120,18 @@ export function transformTeam(record: any): Team {
export function transformTournament(record: any): Tournament {
const teams =
record.expand?.teams
- ?.sort((a: Team, b: Team) =>
+ ?.sort((a: any, b: any) =>
new Date(a.created) < new Date(b.created) ? -1 : 0
)
- ?.map(transformTeam) ?? [];
+ ?.map(transformTeamInfo) ?? [];
+ const matches =
+ record.expand?.matches
+ ?.sort((a: any, b: any) =>
+ a.lid - b.lid ? -1 : 0
+ )
+ ?.map(transformMatch) ?? [];
+
return {
id: record.id,
name: record.name,
@@ -81,5 +145,6 @@ export function transformTournament(record: any): Tournament {
created: record.created,
updated: record.updated,
teams,
+ matches
};
}