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, }); const tournament = await pbAdmin.getTournament(tournamentId); if (!tournament) { throw new Error("Tournament not found"); } if (tournament.matches && tournament.matches.length > 0) { throw new Error("Tournament already has matches generated"); } 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; const seedToTeamId = new Map(); orderedTeamIds.forEach((teamId, index) => { seedToTeamId.set(index + 1, teamId); }); const matchInputs: MatchInput[] = []; 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, status: "tbd", tournament: tournamentId, }; 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; } } if (matchInput.home && matchInput.away) { matchInput.status = "ready"; } matchInputs.push(matchInput); }); }); 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, status: "tbd", tournament: tournamentId, }; matchInputs.push(matchInput); }); }); const createdMatches = await pbAdmin.createMatches(matchInputs); 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, }; }) ); export const startMatch = createServerFn() .validator(z.string()) .middleware([superTokensAdminFunctionMiddleware]) .handler(async ({ data }) => toServerResult(async () => { logger.info("Starting match", data); let match = await pbAdmin.getMatch(data); if (!match) { throw new Error("Match not found"); } match = await pbAdmin.updateMatch(data, { start_time: new Date().toISOString(), status: "started", }); return match; }) ); const endMatchSchema = z.object({ matchId: z.string(), home_cups: z.number(), away_cups: z.number(), ot_count: z.number(), }); export const endMatch = createServerFn() .validator(endMatchSchema) .middleware([superTokensAdminFunctionMiddleware]) .handler(async ({ data: { matchId, home_cups, away_cups, ot_count } }) => toServerResult(async () => { logger.info("Ending match", matchId); let match = await pbAdmin.getMatch(matchId); if (!match) { throw new Error("Match not found"); } match = await pbAdmin.updateMatch(matchId, { end_time: new Date().toISOString(), status: "ended", home_cups, away_cups, ot_count, }); const matchWinner = home_cups > away_cups ? match.home : match.away; const matchLoser = home_cups < away_cups ? match.home : match.away; if (!matchWinner || !matchLoser) throw new Error("Something went wrong"); console.log(matchWinner) console.log(matchLoser) // winner -> where to send match winner to, loser same const { winner, loser } = await pbAdmin.getChildMatches(matchId); // reset match check if (winner && winner.reset) { const awayTeamWon = match.away === matchWinner; if (!awayTeamWon) { // Reset match is not necessary logger.info("Deleting reset match", { resetMatchId: winner.id, currentMatchId: match.id, reason: "not necessary", }); await pbAdmin.deleteMatch(winner.id); return match; } } // advance bracket if (winner) { await pbAdmin.updateMatch(winner.id, { [winner.home_from_lid === match.lid ? "home" : "away"]: matchWinner.id, status: (winner.home_from_lid === match.lid && winner.away) || (winner.away_from_lid === match.lid && winner.home) ? "ready" : "tbd", }); } if (loser) { await pbAdmin.updateMatch(loser.id, { [loser.home_from_lid === match.lid ? "home" : "away"]: matchLoser.id, status: (loser.home_from_lid === match.lid && loser.away) || (loser.away_from_lid === match.lid && loser.home) ? "ready" : "tbd", }); } // TODO: send SSE return match; }) );