social overhaul
This commit is contained in:
@@ -1,5 +1,13 @@
|
||||
import { useServerSuspenseQuery } from "@/lib/tanstack-query/hooks";
|
||||
import { getMatchesBetweenTeams, getMatchesBetweenPlayers } from "./server";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useServerMutation, useServerSuspenseQuery } from "@/lib/tanstack-query/hooks";
|
||||
import { Match } from "@/features/matches/types";
|
||||
import {
|
||||
getMatchesBetweenTeams,
|
||||
getMatchesBetweenPlayers,
|
||||
reportMatchScore,
|
||||
confirmMatchScore,
|
||||
clearMatchReport,
|
||||
} from "./server";
|
||||
|
||||
export const matchKeys = {
|
||||
headToHeadTeams: (team1Id: string, team2Id: string) => ['matches', 'headToHead', 'teams', team1Id, team2Id] as const,
|
||||
@@ -28,3 +36,130 @@ export const usePlayerHeadToHead = (player1Id: string, player2Id: string, enable
|
||||
...matchQueries.headToHeadPlayers(player1Id, player2Id),
|
||||
enabled,
|
||||
});
|
||||
|
||||
const tournamentsRoot = { queryKey: ["tournaments"] as const };
|
||||
|
||||
const reportTeamId = (t: Match["home"]): string | undefined =>
|
||||
!t ? undefined : typeof t === "string" ? t : t.id;
|
||||
|
||||
const reportTeamPlayerIds = (t: Match["home"]): string[] =>
|
||||
t && typeof t !== "string" ? (t.players ?? []).map((p) => p.id) : [];
|
||||
|
||||
function patchTournamentsData(
|
||||
data: unknown,
|
||||
matchId: string,
|
||||
patch: (match: Match) => Partial<Match>
|
||||
): unknown {
|
||||
if (!data) return data;
|
||||
if (Array.isArray(data)) {
|
||||
return data.map((entry) => patchTournamentsData(entry, matchId, patch));
|
||||
}
|
||||
const tournament = data as { matches?: Match[] };
|
||||
if (!Array.isArray(tournament.matches)) return data;
|
||||
|
||||
let changed = false;
|
||||
const matches = tournament.matches.map((m) => {
|
||||
if (m.id !== matchId) return m;
|
||||
changed = true;
|
||||
return { ...m, ...patch(m) };
|
||||
});
|
||||
return changed ? { ...tournament, matches } : data;
|
||||
}
|
||||
|
||||
type MatchVariables = { data: { matchId: string } };
|
||||
type TournamentsSnapshot = [readonly unknown[], unknown][];
|
||||
|
||||
function useOptimisticMatchMutation<TData, TVariables extends MatchVariables>(
|
||||
options: Parameters<typeof useServerMutation<TData, TVariables>>[0] & {
|
||||
buildPatch: (match: Match, variables: TVariables) => Partial<Match>;
|
||||
}
|
||||
) {
|
||||
const queryClient = useQueryClient();
|
||||
const { buildPatch, ...mutationOptions } = options;
|
||||
|
||||
return useServerMutation<TData, TVariables>({
|
||||
...mutationOptions,
|
||||
onMutate: async (variables) => {
|
||||
const { matchId } = variables.data;
|
||||
await queryClient.cancelQueries(tournamentsRoot);
|
||||
|
||||
const previous = queryClient.getQueriesData(tournamentsRoot);
|
||||
queryClient.setQueriesData(tournamentsRoot, (data: unknown) =>
|
||||
patchTournamentsData(data, matchId, (match) => buildPatch(match, variables))
|
||||
);
|
||||
|
||||
return { previous };
|
||||
},
|
||||
onError: (error, variables, onMutateResult, context) => {
|
||||
if (context && typeof context === "object" && "previous" in context) {
|
||||
const snapshot = (context as { previous: TournamentsSnapshot }).previous;
|
||||
for (const [key, data] of snapshot) {
|
||||
queryClient.setQueryData(key, data);
|
||||
}
|
||||
}
|
||||
mutationOptions.onError?.(error, variables, onMutateResult, context);
|
||||
},
|
||||
onSettled: (data, error, variables, onMutateResult, context) => {
|
||||
queryClient.invalidateQueries(tournamentsRoot);
|
||||
mutationOptions.onSettled?.(data, error, variables, onMutateResult, context);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
type MatchMutationOptions = {
|
||||
onSuccess?: Parameters<typeof useServerMutation>[0]["onSuccess"];
|
||||
};
|
||||
|
||||
export const useReportMatchScore = (userId?: string, options?: MatchMutationOptions) =>
|
||||
useOptimisticMatchMutation({
|
||||
mutationFn: reportMatchScore,
|
||||
showSuccessToast: false,
|
||||
...options,
|
||||
buildPatch: (match, variables) => {
|
||||
const onHome = !!userId && reportTeamPlayerIds(match.home).includes(userId);
|
||||
const onAway = !!userId && reportTeamPlayerIds(match.away).includes(userId);
|
||||
const callerTeamId = onHome
|
||||
? reportTeamId(match.home)
|
||||
: onAway
|
||||
? reportTeamId(match.away)
|
||||
: undefined;
|
||||
return {
|
||||
reported_home_cups: variables.data.home_cups,
|
||||
reported_away_cups: variables.data.away_cups,
|
||||
reported_ot_count: variables.data.ot_count,
|
||||
reported_by_team: callerTeamId,
|
||||
reported_by_player: userId,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export const useConfirmMatchScore = (options?: MatchMutationOptions) =>
|
||||
useOptimisticMatchMutation({
|
||||
mutationFn: confirmMatchScore,
|
||||
showSuccessToast: false,
|
||||
...options,
|
||||
buildPatch: (match) => ({
|
||||
status: "ended",
|
||||
home_cups: match.reported_home_cups ?? match.home_cups,
|
||||
away_cups: match.reported_away_cups ?? match.away_cups,
|
||||
ot_count: match.reported_ot_count ?? match.ot_count,
|
||||
reported_home_cups: undefined,
|
||||
reported_away_cups: undefined,
|
||||
reported_ot_count: undefined,
|
||||
reported_by_team: undefined,
|
||||
reported_by_player: undefined,
|
||||
}),
|
||||
});
|
||||
|
||||
export const useClearMatchReport = (options?: MatchMutationOptions) =>
|
||||
useOptimisticMatchMutation({
|
||||
mutationFn: clearMatchReport,
|
||||
...options,
|
||||
buildPatch: () => ({
|
||||
reported_home_cups: undefined,
|
||||
reported_away_cups: undefined,
|
||||
reported_ot_count: undefined,
|
||||
reported_by_team: undefined,
|
||||
reported_by_player: undefined,
|
||||
}),
|
||||
});
|
||||
|
||||
+469
-62
@@ -212,7 +212,6 @@ async function populateKnockoutBracketInternal(tournamentId: string, groupConfig
|
||||
const standings = new Map<string, { teamId: string; wins: number; losses: number; cups_for: number; cups_against: number; cup_differential: number }>();
|
||||
|
||||
for (const team of group.teams || []) {
|
||||
// group.teams can be either team objects or just team ID strings
|
||||
const teamId = typeof team === 'string' ? team : team.id;
|
||||
standings.set(teamId, {
|
||||
teamId,
|
||||
@@ -456,6 +455,344 @@ async function populateKnockoutBracketInternal(tournamentId: string, groupConfig
|
||||
logger.info('Knockout bracket populated successfully', { tournamentId });
|
||||
}
|
||||
|
||||
const teamId = (t: Match["home"] | string | undefined): string | undefined =>
|
||||
!t ? undefined : typeof t === "string" ? t : t.id;
|
||||
|
||||
const winnerId = (m: Match): string | undefined =>
|
||||
m.home_cups > m.away_cups ? teamId(m.home) : teamId(m.away);
|
||||
const loserId = (m: Match): string | undefined =>
|
||||
m.home_cups > m.away_cups ? teamId(m.away) : teamId(m.home);
|
||||
|
||||
function assertValidScore(home_cups: number, away_cups: number, ot_count: number) {
|
||||
if (home_cups === away_cups) throw new Error("A match cannot end in a tie");
|
||||
if (ot_count > 0) return;
|
||||
if (home_cups !== 10 && away_cups !== 10)
|
||||
throw new Error("At least one team must have 10 cups");
|
||||
if (home_cups === 10 && away_cups === 10)
|
||||
throw new Error("Both teams cannot have 10 cups");
|
||||
}
|
||||
|
||||
const CLEARED_RESULT = {
|
||||
home_cups: 0,
|
||||
away_cups: 0,
|
||||
ot_count: 0,
|
||||
start_time: "",
|
||||
end_time: "",
|
||||
reported_home_cups: null,
|
||||
reported_away_cups: null,
|
||||
reported_ot_count: null,
|
||||
reported_by_team: null,
|
||||
reported_by_player: null,
|
||||
} as const;
|
||||
|
||||
function generateRecordId(): string {
|
||||
const alphabet = "abcdefghijklmnopqrstuvwxyz0123456789";
|
||||
let id = "";
|
||||
for (let i = 0; i < 15; i++)
|
||||
id += alphabet[Math.floor(Math.random() * alphabet.length)];
|
||||
return id;
|
||||
}
|
||||
|
||||
type BracketOp =
|
||||
| { kind: "updateMatch"; id: string; data: Record<string, unknown> }
|
||||
| { kind: "createMatch"; data: Record<string, unknown> }
|
||||
| { kind: "deleteMatch"; id: string }
|
||||
| { kind: "updateTournamentMatches"; tournamentId: string; matchIds: string[] };
|
||||
|
||||
function isBatchApiUnavailable(error: unknown): boolean {
|
||||
if (!error || typeof error !== "object") return false;
|
||||
const e = error as { status?: unknown; url?: unknown };
|
||||
const url = typeof e.url === "string" ? e.url : "";
|
||||
if (!url.includes("/api/batch")) return false;
|
||||
return e.status === 403 || e.status === 404;
|
||||
}
|
||||
|
||||
async function commitBracketWrites(ops: BracketOp[]): Promise<void> {
|
||||
const batch = pbAdmin.createBatch();
|
||||
for (const op of ops) {
|
||||
switch (op.kind) {
|
||||
case "updateMatch":
|
||||
batch.collection("matches").update(op.id, op.data);
|
||||
break;
|
||||
case "createMatch":
|
||||
batch.collection("matches").create(op.data);
|
||||
break;
|
||||
case "deleteMatch":
|
||||
batch.collection("matches").delete(op.id);
|
||||
break;
|
||||
case "updateTournamentMatches":
|
||||
batch
|
||||
.collection("tournaments")
|
||||
.update(op.tournamentId, { matches: op.matchIds });
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await batch.send();
|
||||
return;
|
||||
} catch (error) {
|
||||
if (!isBatchApiUnavailable(error)) throw error;
|
||||
logger.warn(
|
||||
"PocketBase batch API is disabled; finalizing match with sequential writes (reduced atomicity). Enable Settings > Application > batch API to restore transactional bracket updates.",
|
||||
);
|
||||
}
|
||||
|
||||
for (const op of ops) {
|
||||
switch (op.kind) {
|
||||
case "updateMatch":
|
||||
await pbAdmin.updateMatch(op.id, op.data as Partial<MatchInput>);
|
||||
break;
|
||||
case "createMatch":
|
||||
await pbAdmin.createMatch(op.data as unknown as MatchInput);
|
||||
break;
|
||||
case "deleteMatch":
|
||||
await pbAdmin.deleteMatch(op.id);
|
||||
break;
|
||||
case "updateTournamentMatches":
|
||||
await pbAdmin.updateTournamentMatches(op.tournamentId, op.matchIds);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function rederiveBracket(
|
||||
tournamentId: string,
|
||||
sourceId: string,
|
||||
score: { home_cups: number; away_cups: number; ot_count: number }
|
||||
): Promise<{ source: Match; downstreamReset: boolean }> {
|
||||
const all = await pbAdmin.getMatchesByTournament(tournamentId);
|
||||
const knockout = all.filter((m) => m.lid >= 0);
|
||||
const byLid = new Map<number, Match>();
|
||||
for (const m of knockout) byLid.set(m.lid, m);
|
||||
|
||||
const original = all.find((m) => m.id === sourceId);
|
||||
if (!original) throw new Error("Match not found");
|
||||
|
||||
const ops: BracketOp[] = [];
|
||||
let warn = false;
|
||||
|
||||
const nowIso = new Date().toISOString();
|
||||
ops.push({
|
||||
kind: "updateMatch",
|
||||
id: sourceId,
|
||||
data: {
|
||||
end_time: nowIso,
|
||||
status: "ended",
|
||||
home_cups: score.home_cups,
|
||||
away_cups: score.away_cups,
|
||||
ot_count: score.ot_count,
|
||||
reported_home_cups: null,
|
||||
reported_away_cups: null,
|
||||
reported_ot_count: null,
|
||||
reported_by_team: null,
|
||||
reported_by_player: null,
|
||||
},
|
||||
});
|
||||
const source: Match = {
|
||||
...original,
|
||||
status: "ended",
|
||||
home_cups: score.home_cups,
|
||||
away_cups: score.away_cups,
|
||||
ot_count: score.ot_count,
|
||||
end_time: nowIso,
|
||||
reported_home_cups: undefined,
|
||||
reported_away_cups: undefined,
|
||||
reported_ot_count: undefined,
|
||||
reported_by_team: undefined,
|
||||
reported_by_player: undefined,
|
||||
};
|
||||
byLid.set(source.lid, source);
|
||||
|
||||
const resolveFeeder = (lid: number, fromLoser: boolean): string | undefined => {
|
||||
const f = byLid.get(lid);
|
||||
if (!f || f.bye || f.status !== "ended") return undefined;
|
||||
return fromLoser ? loserId(f) : winnerId(f);
|
||||
};
|
||||
|
||||
const hasLosers = knockout.some((m) => m.is_losers_bracket);
|
||||
const resetMatch = knockout.find((m) => m.reset);
|
||||
const grandFinal = hasLosers
|
||||
? knockout
|
||||
.filter((m) => !m.reset && !m.bye)
|
||||
.reduce<Match | undefined>(
|
||||
(hi, cur) => (!hi || cur.lid > hi.lid ? cur : hi),
|
||||
undefined
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const ordered = knockout
|
||||
.filter((m) => !m.bye && !m.reset)
|
||||
.sort((a, b) => a.lid - b.lid);
|
||||
|
||||
for (const m of ordered) {
|
||||
if (m.id === sourceId) continue;
|
||||
|
||||
const homeFed = m.home_from_lid >= 0;
|
||||
const awayFed = m.away_from_lid >= 0;
|
||||
if (!homeFed && !awayFed) continue;
|
||||
|
||||
const curHome = teamId(m.home);
|
||||
const curAway = teamId(m.away);
|
||||
const expHome = homeFed ? resolveFeeder(m.home_from_lid, m.home_from_loser) : curHome;
|
||||
const expAway = awayFed ? resolveFeeder(m.away_from_lid, m.away_from_loser) : curAway;
|
||||
const expStatus = expHome && expAway ? "ready" : "tbd";
|
||||
|
||||
const played = m.status === "started" || m.status === "ended";
|
||||
const changed =
|
||||
(homeFed && expHome !== curHome) || (awayFed && expAway !== curAway);
|
||||
|
||||
if (played && changed) {
|
||||
ops.push({
|
||||
kind: "updateMatch",
|
||||
id: m.id,
|
||||
data: {
|
||||
home: expHome ?? null,
|
||||
away: expAway ?? null,
|
||||
status: expStatus,
|
||||
...CLEARED_RESULT,
|
||||
},
|
||||
});
|
||||
byLid.set(m.lid, { ...m, status: expStatus });
|
||||
warn = true;
|
||||
} else if (!played && (changed || m.status !== expStatus)) {
|
||||
ops.push({
|
||||
kind: "updateMatch",
|
||||
id: m.id,
|
||||
data: {
|
||||
home: expHome ?? null,
|
||||
away: expAway ?? null,
|
||||
status: expStatus,
|
||||
},
|
||||
});
|
||||
byLid.set(m.lid, { ...m, status: expStatus });
|
||||
}
|
||||
}
|
||||
|
||||
if (grandFinal) {
|
||||
const gf = byLid.get(grandFinal.lid)!;
|
||||
const gfEnded = gf.status === "ended";
|
||||
const resetNeeded = gfEnded && winnerId(gf) === teamId(gf.away);
|
||||
|
||||
if (resetNeeded) {
|
||||
const expHome = winnerId(gf);
|
||||
const expAway = loserId(gf);
|
||||
|
||||
if (resetMatch && resetMatch.id !== sourceId) {
|
||||
const played =
|
||||
resetMatch.status === "started" || resetMatch.status === "ended";
|
||||
const changed =
|
||||
teamId(resetMatch.home) !== expHome || teamId(resetMatch.away) !== expAway;
|
||||
if (played && changed) {
|
||||
ops.push({
|
||||
kind: "updateMatch",
|
||||
id: resetMatch.id,
|
||||
data: {
|
||||
home: expHome ?? null,
|
||||
away: expAway ?? null,
|
||||
status: "ready",
|
||||
...CLEARED_RESULT,
|
||||
},
|
||||
});
|
||||
warn = true;
|
||||
} else if (!played && (changed || resetMatch.status !== "ready")) {
|
||||
ops.push({
|
||||
kind: "updateMatch",
|
||||
id: resetMatch.id,
|
||||
data: {
|
||||
home: expHome ?? null,
|
||||
away: expAway ?? null,
|
||||
status: "ready",
|
||||
},
|
||||
});
|
||||
}
|
||||
} else if (!resetMatch) {
|
||||
const newId = generateRecordId();
|
||||
ops.push({
|
||||
kind: "createMatch",
|
||||
data: {
|
||||
id: newId,
|
||||
lid: gf.lid + 1,
|
||||
order: gf.order + 1,
|
||||
round: gf.round + 1,
|
||||
reset: true,
|
||||
bye: false,
|
||||
home_cups: 0,
|
||||
away_cups: 0,
|
||||
ot_count: 0,
|
||||
home_from_lid: gf.lid,
|
||||
away_from_lid: gf.lid,
|
||||
home_from_loser: false,
|
||||
away_from_loser: true,
|
||||
is_losers_bracket: false,
|
||||
status: "ready",
|
||||
tournament: tournamentId,
|
||||
home: expHome ?? undefined,
|
||||
away: expAway ?? undefined,
|
||||
},
|
||||
});
|
||||
ops.push({
|
||||
kind: "updateTournamentMatches",
|
||||
tournamentId,
|
||||
matchIds: [...all.map((m) => m.id), newId],
|
||||
});
|
||||
}
|
||||
} else if (resetMatch && resetMatch.id !== sourceId) {
|
||||
if (resetMatch.status === "started" || resetMatch.status === "ended") {
|
||||
warn = true;
|
||||
}
|
||||
ops.push({ kind: "deleteMatch", id: resetMatch.id });
|
||||
}
|
||||
}
|
||||
|
||||
await commitBracketWrites(ops);
|
||||
|
||||
return { source, downstreamReset: warn };
|
||||
}
|
||||
|
||||
async function finalizeMatch(
|
||||
matchId: string,
|
||||
{ home_cups, away_cups, ot_count }: { home_cups: number; away_cups: number; ot_count: number }
|
||||
): Promise<{ match: Match; downstreamReset: boolean; groupEditAfterKnockout: boolean }> {
|
||||
assertValidScore(home_cups, away_cups, ot_count);
|
||||
|
||||
const existing = await pbAdmin.getMatch(matchId);
|
||||
if (!existing) throw new Error("Match not found");
|
||||
const tournamentId = existing.tournament.id;
|
||||
|
||||
if (existing.lid === -1) {
|
||||
const source = await pbAdmin.updateMatch(matchId, {
|
||||
end_time: new Date().toISOString(),
|
||||
status: "ended",
|
||||
home_cups,
|
||||
away_cups,
|
||||
ot_count,
|
||||
reported_home_cups: null,
|
||||
reported_away_cups: null,
|
||||
reported_ot_count: null,
|
||||
reported_by_team: null,
|
||||
reported_by_player: null,
|
||||
});
|
||||
|
||||
const all = await pbAdmin.getMatchesByTournament(tournamentId);
|
||||
const groupEditAfterKnockout = all.some(
|
||||
(m) => m.round >= 0 && (teamId(m.home) || teamId(m.away))
|
||||
);
|
||||
|
||||
emitServerEvent({ type: "match", matchId: source.id, tournamentId });
|
||||
return { match: source, downstreamReset: false, groupEditAfterKnockout };
|
||||
}
|
||||
|
||||
const { source, downstreamReset } = await rederiveBracket(tournamentId, matchId, {
|
||||
home_cups,
|
||||
away_cups,
|
||||
ot_count,
|
||||
});
|
||||
|
||||
emitServerEvent({ type: "match", matchId: source.id, tournamentId });
|
||||
return { match: source, downstreamReset, groupEditAfterKnockout: false };
|
||||
}
|
||||
|
||||
const endMatchSchema = z.object({
|
||||
matchId: z.string(),
|
||||
home_cups: z.number(),
|
||||
@@ -469,83 +806,153 @@ export const endMatch = createServerFn()
|
||||
toServerResult(async () => {
|
||||
logger.info("Ending match", matchId);
|
||||
|
||||
let match = await pbAdmin.getMatch(matchId);
|
||||
const 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,
|
||||
return finalizeMatch(matchId, { home_cups, away_cups, ot_count });
|
||||
})
|
||||
);
|
||||
|
||||
const teamPlayerIds = (team: Match["home"]): string[] =>
|
||||
team && typeof team !== "string" ? (team.players ?? []).map((p) => p.id) : [];
|
||||
|
||||
const reportScoreSchema = z.object({
|
||||
matchId: z.string(),
|
||||
home_cups: z.number(),
|
||||
away_cups: z.number(),
|
||||
ot_count: z.number(),
|
||||
});
|
||||
|
||||
export const reportMatchScore = createServerFn()
|
||||
.validator(reportScoreSchema)
|
||||
.middleware([superTokensFunctionMiddleware, serverFnLoggingMiddleware])
|
||||
.handler(async ({ data: { matchId, home_cups, away_cups, ot_count }, context }) =>
|
||||
toServerResult(async () => {
|
||||
const player = await pbAdmin.getPlayerByAuthId(context.userAuthId);
|
||||
if (!player?.id) throw new Error("Player not found");
|
||||
|
||||
const match = await pbAdmin.getMatch(matchId);
|
||||
if (!match) throw new Error("Match not found");
|
||||
if (match.status !== "started")
|
||||
throw new Error("Scores can only be reported once the match has started");
|
||||
|
||||
const onHome = teamPlayerIds(match.home).includes(player.id);
|
||||
const onAway = teamPlayerIds(match.away).includes(player.id);
|
||||
if (!onHome && !onAway)
|
||||
throw new Error("You are not a player in this match");
|
||||
if (onHome && onAway)
|
||||
throw new Error(
|
||||
"You are on both teams in this match and cannot report or confirm its score"
|
||||
);
|
||||
|
||||
assertValidScore(home_cups, away_cups, ot_count);
|
||||
|
||||
const callerTeamId = onHome ? teamId(match.home) : teamId(match.away);
|
||||
|
||||
const hasPending =
|
||||
match.reported_by_team != null &&
|
||||
match.reported_home_cups != null &&
|
||||
match.reported_away_cups != null;
|
||||
const fromOppositeTeam =
|
||||
hasPending && match.reported_by_team !== callerTeamId;
|
||||
const identical =
|
||||
hasPending &&
|
||||
match.reported_home_cups === home_cups &&
|
||||
match.reported_away_cups === away_cups &&
|
||||
(match.reported_ot_count ?? 0) === ot_count;
|
||||
|
||||
if (fromOppositeTeam && identical) {
|
||||
const result = await finalizeMatch(matchId, { home_cups, away_cups, ot_count });
|
||||
return {
|
||||
success: true as const,
|
||||
finalized: true as const,
|
||||
downstreamReset: result.downstreamReset,
|
||||
groupEditAfterKnockout: result.groupEditAfterKnockout,
|
||||
};
|
||||
}
|
||||
|
||||
await pbAdmin.updateMatch(matchId, {
|
||||
reported_home_cups: home_cups,
|
||||
reported_away_cups: away_cups,
|
||||
reported_ot_count: ot_count,
|
||||
reported_by_team: callerTeamId,
|
||||
reported_by_player: player.id,
|
||||
});
|
||||
|
||||
if (match.lid === -1) {
|
||||
emitServerEvent({
|
||||
type: "match",
|
||||
matchId: match.id,
|
||||
tournamentId: match.tournament.id
|
||||
});
|
||||
return match;
|
||||
}
|
||||
emitServerEvent({ type: "match", matchId, tournamentId: match.tournament.id });
|
||||
return { success: true as const, finalized: false as const };
|
||||
})
|
||||
);
|
||||
|
||||
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");
|
||||
const matchIdSchema = z.object({ matchId: z.string() });
|
||||
|
||||
const { winner, loser } = await pbAdmin.getChildMatches(matchId);
|
||||
export const confirmMatchScore = createServerFn()
|
||||
.validator(matchIdSchema)
|
||||
.middleware([superTokensFunctionMiddleware, serverFnLoggingMiddleware])
|
||||
.handler(async ({ data: { matchId }, context }) =>
|
||||
toServerResult(async () => {
|
||||
const player = await pbAdmin.getPlayerByAuthId(context.userAuthId);
|
||||
if (!player?.id) throw new Error("Player not found");
|
||||
|
||||
if (winner && winner.reset) {
|
||||
const awayTeamWon = match.away === matchWinner;
|
||||
const match = await pbAdmin.getMatch(matchId);
|
||||
if (!match) throw new Error("Match not found");
|
||||
if (match.status !== "started")
|
||||
throw new Error("Match is not in progress");
|
||||
if (!match.reported_by_team)
|
||||
throw new Error("No score has been reported yet");
|
||||
if (match.reported_home_cups == null || match.reported_away_cups == null)
|
||||
throw new Error("The reported score is incomplete");
|
||||
|
||||
if (!awayTeamWon) {
|
||||
logger.info("Deleting reset match", {
|
||||
resetMatchId: winner.id,
|
||||
currentMatchId: match.id,
|
||||
reason: "not necessary",
|
||||
});
|
||||
const onHome = teamPlayerIds(match.home).includes(player.id);
|
||||
const onAway = teamPlayerIds(match.away).includes(player.id);
|
||||
if (onHome && onAway)
|
||||
throw new Error(
|
||||
"You are on both teams in this match and cannot report or confirm its score"
|
||||
);
|
||||
|
||||
await pbAdmin.deleteMatch(winner.id);
|
||||
emitServerEvent({
|
||||
type: "match",
|
||||
matchId: match.id,
|
||||
tournamentId: match.tournament.id
|
||||
});
|
||||
return match;
|
||||
}
|
||||
}
|
||||
const homeId = teamId(match.home);
|
||||
const reportedByHome = match.reported_by_team === homeId;
|
||||
const opposingTeam = reportedByHome ? match.away : match.home;
|
||||
|
||||
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 (!teamPlayerIds(opposingTeam).includes(player.id))
|
||||
throw new Error("Only a player on the other team can confirm this score");
|
||||
|
||||
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",
|
||||
});
|
||||
}
|
||||
return finalizeMatch(matchId, {
|
||||
home_cups: match.reported_home_cups,
|
||||
away_cups: match.reported_away_cups,
|
||||
ot_count: match.reported_ot_count ?? 0,
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
emitServerEvent({
|
||||
type: "match",
|
||||
matchId: match.id,
|
||||
tournamentId: match.tournament.id
|
||||
export const clearMatchReport = createServerFn()
|
||||
.validator(matchIdSchema)
|
||||
.middleware([superTokensFunctionMiddleware, serverFnLoggingMiddleware])
|
||||
.handler(async ({ data: { matchId }, context }) =>
|
||||
toServerResult(async () => {
|
||||
const player = await pbAdmin.getPlayerByAuthId(context.userAuthId);
|
||||
if (!player?.id) throw new Error("Player not found");
|
||||
|
||||
const match = await pbAdmin.getMatch(matchId);
|
||||
if (!match) throw new Error("Match not found");
|
||||
|
||||
const onHome = teamPlayerIds(match.home).includes(player.id);
|
||||
const onAway = teamPlayerIds(match.away).includes(player.id);
|
||||
if (!onHome && !onAway)
|
||||
throw new Error("You are not a player in this match");
|
||||
|
||||
await pbAdmin.updateMatch(matchId, {
|
||||
reported_home_cups: null,
|
||||
reported_away_cups: null,
|
||||
reported_ot_count: null,
|
||||
reported_by_team: null,
|
||||
reported_by_player: null,
|
||||
});
|
||||
|
||||
return match;
|
||||
emitServerEvent({ type: "match", matchId, tournamentId: match.tournament.id });
|
||||
return { success: true };
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -32,6 +32,11 @@ export interface Match {
|
||||
away_seed?: number;
|
||||
match_type?: MatchType;
|
||||
group?: string;
|
||||
reported_home_cups?: number;
|
||||
reported_away_cups?: number;
|
||||
reported_ot_count?: number;
|
||||
reported_by_team?: string;
|
||||
reported_by_player?: string;
|
||||
}
|
||||
|
||||
export const matchInputSchema = z.object({
|
||||
@@ -52,12 +57,17 @@ export const matchInputSchema = z.object({
|
||||
is_losers_bracket: z.boolean().optional().default(false),
|
||||
status: z.enum(["tbd", "ready", "started", "ended"]).optional().default("tbd"),
|
||||
tournament: z.string().min(1),
|
||||
home: z.string().min(1).optional(),
|
||||
away: z.string().min(1).optional(),
|
||||
home: z.string().min(1).nullable().optional(),
|
||||
away: z.string().min(1).nullable().optional(),
|
||||
home_seed: z.number().int().min(1).optional(),
|
||||
away_seed: z.number().int().min(1).optional(),
|
||||
match_type: z.enum(["group_stage", "knockout", "winners", "losers", "bracket"]).optional(),
|
||||
group: z.string().optional(),
|
||||
reported_home_cups: z.number().int().min(0).nullable().optional(),
|
||||
reported_away_cups: z.number().int().min(0).nullable().optional(),
|
||||
reported_ot_count: z.number().int().min(0).nullable().optional(),
|
||||
reported_by_team: z.string().nullable().optional(),
|
||||
reported_by_player: z.string().nullable().optional(),
|
||||
});
|
||||
|
||||
export type MatchInput = z.infer<typeof matchInputSchema>;
|
||||
|
||||
Reference in New Issue
Block a user