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"; import { serverEvents } from "@/lib/events/emitter"; import { superTokensFunctionMiddleware } from "@/utils/supertokens"; import { PlayerInfo } from "../players/types"; import { serverFnLoggingMiddleware } from "@/utils/activities"; const orderedTeamsSchema = z.object({ tournamentId: z.string(), orderedTeamIds: z.array(z.string()), }); export const generateTournamentBracket = createServerFn() .inputValidator(orderedTeamsSchema) .middleware([superTokensAdminFunctionMiddleware, serverFnLoggingMiddleware]) .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 === null ? -1 : match.home_from_lid, away_from_lid: match.away_from_lid === null ? -1 : 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 === null ? -1 : match.home_from_lid, away_from_lid: match.away_from_lid === null ? -1 : 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() .inputValidator(z.string()) .middleware([superTokensAdminFunctionMiddleware, serverFnLoggingMiddleware]) .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", }); serverEvents.emit("match", { type: "match", matchId: match.id, tournamentId: match.tournament.id }); 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() .inputValidator(endMatchSchema) .middleware([superTokensAdminFunctionMiddleware, serverFnLoggingMiddleware]) .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"); // 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", }); } serverEvents.emit("match", { type: "match", matchId: match.id, tournamentId: match.tournament.id }); return match; }) ); const toggleReactionSchema = z.object({ matchId: z.string(), emoji: z.string(), }); export const toggleMatchReaction = createServerFn() .inputValidator(toggleReactionSchema) .middleware([superTokensFunctionMiddleware]) .handler(async ({ data: { matchId, emoji }, context }) => toServerResult(async () => { const user = await pbAdmin.getPlayerByAuthId(context.userAuthId); const userId = user?.id; if (!userId) return; const match = await pbAdmin.getMatch(matchId); if (!match) { throw new Error("Match not found"); } const existingReaction = await pbAdmin.getUserReaction(matchId, userId, emoji); if (existingReaction) { await pbAdmin.deleteReaction(existingReaction.id); logger.info("Removed reaction", { matchId, emoji, userId }); } else { await pbAdmin.createReaction(matchId, userId, emoji); logger.info("Added reaction", { matchId, emoji, userId }); } const all = await pbAdmin.getReactionsForMatch(matchId); const reactionsByEmoji = all.reduce((acc, reaction) => { const emoji = reaction.emoji; if (!acc[emoji]) { acc[emoji] = { emoji, count: 0, players: [], }; } acc[emoji].count++; acc[emoji].players.push({ id: reaction?.player?.id, first_name: reaction.player?.first_name, last_name: reaction.player?.last_name, }); return acc; }, {} as Record); const reactions = Object.values(reactionsByEmoji); serverEvents.emit("reaction", { type: "reaction", matchId, reactions, }); return reactions as Reaction[] }) ); export interface Reaction { emoji: string; count: number; players: PlayerInfo[]; } export const getMatchReactions = createServerFn() .inputValidator(z.string()) .middleware([superTokensFunctionMiddleware]) .handler(async ({ data: matchId, context }) => toServerResult(async () => { const match = await pbAdmin.getMatch(matchId); if (!match) { throw new Error("Match not found"); } const all = await pbAdmin.getReactionsForMatch(matchId); const reactionsByEmoji = all.reduce((acc, reaction) => { const emoji = reaction.emoji; if (!acc[emoji]) { acc[emoji] = { emoji, count: 0, players: [], }; } acc[emoji].count++; acc[emoji].players.push({ id: reaction?.player?.id, first_name: reaction.player?.first_name, last_name: reaction.player?.last_name, }); return acc; }, {} as Record); const reactions = Object.values(reactionsByEmoji); return reactions as Reaction[] }) ); const matchesBetweenPlayersSchema = z.object({ player1Id: z.string(), player2Id: z.string(), }); export const getMatchesBetweenPlayers = createServerFn() .inputValidator(matchesBetweenPlayersSchema) .middleware([superTokensFunctionMiddleware]) .handler(async ({ data: { player1Id, player2Id } }) => toServerResult(async () => { logger.info("Getting matches between players", { player1Id, player2Id }); const matches = await pbAdmin.getMatchesBetweenPlayers(player1Id, player2Id); return matches; }) ); const matchesBetweenTeamsSchema = z.object({ team1Id: z.string(), team2Id: z.string(), }); export const getMatchesBetweenTeams = createServerFn() .inputValidator(matchesBetweenTeamsSchema) .middleware([superTokensFunctionMiddleware]) .handler(async ({ data: { team1Id, team2Id } }) => toServerResult(async () => { logger.info("Getting matches between teams", { team1Id, team2Id }); const matches = await pbAdmin.getMatchesBetweenTeams(team1Id, team2Id); return matches; }) );