From e3fae4fb0a9de2944cb0a5ffa69ba083cb795933 Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Fri, 2 Oct 2026 18:40:47 +0300 Subject: [PATCH] Fix browser back button crash on tournament page --- .../tournament-bracket/core/Bracket.test.ts | 36 +++++++++++++++++++ .../core/Bracket/RoundRobinBracket.ts | 2 ++ .../core/Bracket/SwissBracket.ts | 2 ++ .../routes/to.$id.brackets.tsx | 15 ++++++++ ...026-10-02-brackets-page-crash-new-teams.md | 5 +++ 5 files changed, 60 insertions(+) create mode 100644 changelog/2026-10-02-brackets-page-crash-new-teams.md diff --git a/app/features/tournament-bracket/core/Bracket.test.ts b/app/features/tournament-bracket/core/Bracket.test.ts index 0f6cb173a..6af4486eb 100644 --- a/app/features/tournament-bracket/core/Bracket.test.ts +++ b/app/features/tournament-bracket/core/Bracket.test.ts @@ -322,6 +322,42 @@ describe("swiss standings - losses against tied", () => { }); }); +describe("live standings - teams the tournament does not know", () => { + // client-side the bracket's match data can be fresher than the layout's teams + test.each(["swiss", "round_robin"] as const)( + "leaves them out (%s)", + (type) => { + const tournament = testTournament({ + ctx: { + settings: { + bracketProgression: [ + { + type, + name: "Bracket", + requiresCheckIn: false, + settings: {}, + sources: [], + }, + ], + }, + teams: [tournamentCtxTeam(1), tournamentCtxTeam(2)], + }, + data: Engine.create({ + type, + seeding: [1, 2, 3], + settings: { groupCount: 1, roundCount: 3 }, + }), + }); + + const standings = tournament.bracketByIdx(0)!.liveStandings; + + expect(standings.map((standing) => standing.team.id).sort()).toEqual([ + 1, 2, + ]); + }, + ); +}); + describe("swiss standings - cross group ties", () => { // Two Swiss groups of four playing one round. Group 1 is won by seed 3 (who beat // seed 7), group 2 by seed 6 (who upset seed 2). Both are 1st of their group, and diff --git a/app/features/tournament-bracket/core/Bracket/RoundRobinBracket.ts b/app/features/tournament-bracket/core/Bracket/RoundRobinBracket.ts index d42704c63..916247970 100644 --- a/app/features/tournament-bracket/core/Bracket/RoundRobinBracket.ts +++ b/app/features/tournament-bracket/core/Bracket/RoundRobinBracket.ts @@ -279,6 +279,8 @@ export class RoundRobinBracket extends Bracket { placements.push( ...teams + // client-side the layout's teams can lag behind freshly loaded match data, mostly preview tournaments (prevent crash) + .filter((team) => this.tournament.teamById(team.id)) .sort((a, b) => { // TIEBREAKER 0) dropped out teams are always last const aDroppedOut = droppedOutTeams.includes(a.id); diff --git a/app/features/tournament-bracket/core/Bracket/SwissBracket.ts b/app/features/tournament-bracket/core/Bracket/SwissBracket.ts index 796d4c46b..53b4e30cf 100644 --- a/app/features/tournament-bracket/core/Bracket/SwissBracket.ts +++ b/app/features/tournament-bracket/core/Bracket/SwissBracket.ts @@ -376,6 +376,8 @@ export class SwissBracket extends Bracket { placements.push( ...teams + // client-side the layout's teams can lag behind freshly loaded match data, mostly preview tournaments (prevent crash) + .filter((team) => this.tournament.teamById(team.id)) .sort((a, b) => { // TIEBREAKER 0) dropped out teams are always last const aDroppedOut = droppedOutTeams.includes(a.id); diff --git a/app/features/tournament-bracket/routes/to.$id.brackets.tsx b/app/features/tournament-bracket/routes/to.$id.brackets.tsx index a80bac424..d36205922 100644 --- a/app/features/tournament-bracket/routes/to.$id.brackets.tsx +++ b/app/features/tournament-bracket/routes/to.$id.brackets.tsx @@ -16,6 +16,7 @@ import { useLoaderData, useLocation, useOutletContext, + useRevalidator, } from "react-router"; import { Alert } from "~/components/Alert"; import { LinkButton, SendouButton } from "~/components/elements/Button"; @@ -102,6 +103,7 @@ function TournamentBracketsView() { useScrollToMatchOnLoad(); const bracket = tournament.bracketByIdx(data.bracketIdx); + useRevalidateOnUnknownTeams(bracket); useTopicRevalidation( tournamentChannel(tournament.ctx.id), @@ -281,6 +283,19 @@ function useScrollToMatchOnLoad() { }, [scrollToMatchId]); } +/** The layout does not revalidate on navigations within the tournament, so it can lack teams registered since it loaded. */ +function useRevalidateOnUnknownTeams(bracket: BracketType | null) { + const { revalidate } = useRevalidator(); + const hasUnknownTeams = + bracket?.participantTournamentTeamIds.some( + (teamId) => !bracket.tournament.teamById(teamId), + ) ?? false; + + React.useEffect(() => { + if (hasUnknownTeams) revalidate(); + }, [hasUnknownTeams, revalidate]); +} + function getAbDivisionsStartError( bracket: BracketType, tournament: Tournament, diff --git a/changelog/2026-10-02-brackets-page-crash-new-teams.md b/changelog/2026-10-02-brackets-page-crash-new-teams.md new file mode 100644 index 000000000..86245663d --- /dev/null +++ b/changelog/2026-10-02-brackets-page-crash-new-teams.md @@ -0,0 +1,5 @@ +--- +navItem: medal +type: bug +--- +Fix the tournament brackets page crashing when returning to it after new teams had registered (using browser back button)