From 49e35fd4c2c47342d773868898fc5171bec50ccf Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Sun, 26 Apr 2026 20:01:58 +0300 Subject: [PATCH] Fix A/B round robin only tournament placements on front page Closes #3009 --- app/features/calendar/calendar-types.ts | 4 +- .../calendar/components/TournamentCard.tsx | 85 ++++++++++++++--- .../core/ShowcaseTournaments.server.ts | 93 ++++++++++++------- 3 files changed, 133 insertions(+), 49 deletions(-) diff --git a/app/features/calendar/calendar-types.ts b/app/features/calendar/calendar-types.ts index 263e67b87..5782ec19b 100644 --- a/app/features/calendar/calendar-types.ts +++ b/app/features/calendar/calendar-types.ts @@ -46,13 +46,13 @@ export interface ShowcaseCalendarEvent extends CommonEvent { hidden: boolean; isFinalized: boolean; minMembersPerTeam: number; - firstPlacer: { + firstPlacers: Array<{ teamName: string; logoUrl: string | null; members: (CommonUser & { country: Tables["User"]["country"] })[]; notShownMembersCount: number; div: string | null; - } | null; + }>; hasVods?: boolean; } diff --git a/app/features/calendar/components/TournamentCard.tsx b/app/features/calendar/components/TournamentCard.tsx index a90b5cd81..6682fa624 100644 --- a/app/features/calendar/components/TournamentCard.tsx +++ b/app/features/calendar/components/TournamentCard.tsx @@ -57,7 +57,8 @@ export function TournamentCard({ return (
0, })} data-testid="tournament-card" > @@ -117,15 +118,17 @@ export function TournamentCard({
) : null} - {isShowcase && tournament.firstPlacer ? ( + {isShowcase && tournament.firstPlacers.length > 0 ? ( ) : null}
- {isShowcase && tournament.firstPlacer && isCensored(tournament.id) ? ( + {isShowcase && + tournament.firstPlacers.length > 0 && + isCensored(tournament.id) ? ( reveal(tournament.id)} /> ) : null} {isShowcase && "hasVods" in tournament && tournament.hasVods ? ( @@ -157,20 +160,52 @@ export function TournamentCard({ } function TournamentFirstPlacers({ - firstPlacer, + firstPlacers, censored, }: { - firstPlacer: NonNullable; + firstPlacers: ShowcaseCalendarEvent["firstPlacers"]; + censored: boolean; +}) { + if (firstPlacers.length > 1) { + return ( +
+
+ {firstPlacers.map((placer) => ( + + ))} +
+
+ ); + } + + const placer = firstPlacers[0]; + + return ( +
+ +
+ ); +} + +function TournamentFirstPlacerWithMembers({ + placer, + censored, +}: { + placer: ShowcaseCalendarEvent["firstPlacers"][number]; censored: boolean; }) { const { t } = useTranslation(["front"]); return ( -
+ <>
- {!censored && firstPlacer.logoUrl ? ( + {!censored && placer.logoUrl ? ( - {censored ? "???" : firstPlacer.teamName} + {censored ? "???" : placer.teamName}
{t("front:showcase.card.winner")} - {firstPlacer.div ? ` (${firstPlacer.div})` : null} + {placer.div ? ` (${placer.div})` : null}
- {firstPlacer.members.map((member) => ( + {placer.members.map((member) => (
{!censored && member.country ? ( @@ -195,12 +230,34 @@ function TournamentFirstPlacers({ {censored ? "???" : member.username}{" "}
))} - {!censored && firstPlacer.notShownMembersCount > 0 ? ( + {!censored && placer.notShownMembersCount > 0 ? (
- +{firstPlacer.notShownMembersCount} + +{placer.notShownMembersCount}
) : null}
+ + ); +} + +function TournamentFirstPlacerTeamNameOnly({ + placer, + censored, +}: { + placer: ShowcaseCalendarEvent["firstPlacers"][number]; + censored: boolean; +}) { + const { t } = useTranslation(["front"]); + + return ( +
+ + {censored ? "???" : placer.teamName} + +
+ {t("front:showcase.card.winner")} + {placer.div ? ` (${placer.div})` : null} +
); } diff --git a/app/features/front-page/core/ShowcaseTournaments.server.ts b/app/features/front-page/core/ShowcaseTournaments.server.ts index 2064eef28..792652114 100644 --- a/app/features/front-page/core/ShowcaseTournaments.server.ts +++ b/app/features/front-page/core/ShowcaseTournaments.server.ts @@ -1,10 +1,12 @@ import cachified from "@epic-web/cachified"; +import * as R from "remeda"; import type { ShowcaseCalendarEvent } from "~/features/calendar/calendar-types"; import * as TournamentRepository from "~/features/tournament/TournamentRepository.server"; import { getBracketProgressionLabel, tournamentIsRanked, } from "~/features/tournament/tournament-utils"; +import * as Progression from "~/features/tournament-bracket/core/Progression"; import { getTentativeTier } from "~/features/tournament-organization/core/tentativeTiers.server"; import { cache, IN_MILLISECONDS, ttl } from "~/utils/cache.server"; import { @@ -186,16 +188,21 @@ function deleteExtraResults(tournaments: ShowcaseCalendarEvent[]) { const threeDaysAgo = databaseTimestampThreeDaysAgo(); const nonResults = tournaments.filter( (tournament) => - !tournament.firstPlacer && + tournament.firstPlacers.length === 0 && !tournament.isFinalized && tournament.startTime > threeDaysAgo, ); const rankedResults = tournaments - .filter((tournament) => tournament.firstPlacer && tournament.isRanked) + .filter( + (tournament) => tournament.firstPlacers.length > 0 && tournament.isRanked, + ) .sort((a, b) => showcaseScore(b) - showcaseScore(a)); const nonRankedResults = tournaments - .filter((tournament) => tournament.firstPlacer && !tournament.isRanked) + .filter( + (tournament) => + tournament.firstPlacers.length > 0 && !tournament.isRanked, + ) .sort((a, b) => showcaseScore(b) - showcaseScore(a)); const rankedResultsToKeep = rankedResults.slice(0, 4); @@ -283,7 +290,7 @@ const MEMBERS_TO_SHOW = 5; function mapTournamentFromDB( tournament: TournamentRepository.ForShowcase, ): ShowcaseCalendarEvent { - const highestDivWinners = resolveHighestDivisionWinners(tournament); + const firstPlacers = resolveFirstPlacers(tournament); const tentativeTier = tournament.tier === null && @@ -320,41 +327,35 @@ function mapTournamentFromDB( minMembersPerTeam: tournament.settings.minMembersPerTeam ?? 4, modes: null, hasVods: (tournament.vodCount ?? 0) > 0, - firstPlacer: - highestDivWinners.length > 0 - ? { - teamName: highestDivWinners[0].teamName, - logoUrl: - highestDivWinners[0].teamLogoUrl ?? - highestDivWinners[0].pickupAvatarUrl, - div: highestDivWinners[0].div, - members: highestDivWinners - .slice(0, MEMBERS_TO_SHOW) - .map((firstPlacer) => ({ - customUrl: firstPlacer.customUrl, - discordAvatar: firstPlacer.discordAvatar, - discordId: firstPlacer.discordId, - id: firstPlacer.id, - username: firstPlacer.username, - country: firstPlacer.country, - })), - notShownMembersCount: - highestDivWinners.length > MEMBERS_TO_SHOW - ? highestDivWinners.length - MEMBERS_TO_SHOW - : 0, - } - : null, + firstPlacers, }; } -function resolveHighestDivisionWinners( +type FirstPlacerRow = TournamentRepository.ForShowcase["firstPlacers"][number]; + +function resolveFirstPlacers( tournament: TournamentRepository.ForShowcase, -) { +): ShowcaseCalendarEvent["firstPlacers"] { if (tournament.firstPlacers.length === 0) { return []; } - // not a "many starting brackets" tournament + if ( + Progression.hasAbDivisionsFinals(tournament.settings.bracketProgression) + ) { + const byDiv = R.groupBy(tournament.firstPlacers, (p) => p.div ?? ""); + return Object.values(byDiv) + .map((rows) => buildFirstPlacerEntry(rows, { withMembers: false })) + .sort((a, b) => (a.div ?? "").localeCompare(b.div ?? "")); + } + + const winnerRows = winnersOfHighestDivision(tournament); + return [buildFirstPlacerEntry(winnerRows, { withMembers: true })]; +} + +function winnersOfHighestDivision( + tournament: TournamentRepository.ForShowcase, +): FirstPlacerRow[] { if (tournament.firstPlacers.every((p) => p.div === null)) { return tournament.firstPlacers; } @@ -363,8 +364,6 @@ function resolveHighestDivisionWinners( 0, tournament.settings.bracketProgression, ); - - // Filter to only include winners from the highest division const highestDivWinners = tournament.firstPlacers.filter( (p) => p.div === highestDivName, ); @@ -374,6 +373,34 @@ function resolveHighestDivisionWinners( : tournament.firstPlacers; } +function buildFirstPlacerEntry( + rows: FirstPlacerRow[], + { withMembers }: { withMembers: boolean }, +): ShowcaseCalendarEvent["firstPlacers"][number] { + const first = rows[0]; + const members = withMembers + ? rows.slice(0, MEMBERS_TO_SHOW).map((row) => ({ + customUrl: row.customUrl, + discordAvatar: row.discordAvatar, + discordId: row.discordId, + id: row.id, + username: row.username, + country: row.country, + })) + : []; + + return { + teamName: first.teamName, + logoUrl: first.teamLogoUrl ?? first.pickupAvatarUrl, + div: first.div, + members, + notShownMembersCount: + withMembers && rows.length > MEMBERS_TO_SHOW + ? rows.length - MEMBERS_TO_SHOW + : 0, + }; +} + function databaseTimestampWeekFromNow() { const now = new Date();