Fix A/B round robin only tournament placements on front page
Some checks are pending
E2E Tests / e2e (push) Waiting to run
Tests and checks on push / run-checks-and-tests (push) Waiting to run
Updates translation progress / update-translation-progress-issue (push) Waiting to run

Closes #3009
This commit is contained in:
Kalle 2026-04-26 20:01:58 +03:00
parent a8a42797e5
commit 49e35fd4c2
3 changed files with 133 additions and 49 deletions

View File

@ -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;
}

View File

@ -57,7 +57,8 @@ export function TournamentCard({
return (
<div
className={clsx(className, styles.container, {
[styles.containerTall]: isShowcase && tournament.firstPlacer,
[styles.containerTall]:
isShowcase && tournament.firstPlacers.length > 0,
})}
data-testid="tournament-card"
>
@ -117,15 +118,17 @@ export function TournamentCard({
<Tags tags={tournament.tags} small centered />
</div>
) : null}
{isShowcase && tournament.firstPlacer ? (
{isShowcase && tournament.firstPlacers.length > 0 ? (
<TournamentFirstPlacers
firstPlacer={tournament.firstPlacer}
firstPlacers={tournament.firstPlacers}
censored={isCensored(tournament.id)}
/>
) : null}
</Link>
<div className="stack horizontal justify-between items-center">
{isShowcase && tournament.firstPlacer && isCensored(tournament.id) ? (
{isShowcase &&
tournament.firstPlacers.length > 0 &&
isCensored(tournament.id) ? (
<SpoilerRevealPill onReveal={() => reveal(tournament.id)} />
) : null}
{isShowcase && "hasVods" in tournament && tournament.hasVods ? (
@ -157,20 +160,52 @@ export function TournamentCard({
}
function TournamentFirstPlacers({
firstPlacer,
firstPlacers,
censored,
}: {
firstPlacer: NonNullable<ShowcaseCalendarEvent["firstPlacer"]>;
firstPlacers: ShowcaseCalendarEvent["firstPlacers"];
censored: boolean;
}) {
if (firstPlacers.length > 1) {
return (
<div className={styles.firstPlacers}>
<div className="stack md items-start">
{firstPlacers.map((placer) => (
<TournamentFirstPlacerTeamNameOnly
key={placer.div ?? placer.teamName}
placer={placer}
censored={censored}
/>
))}
</div>
</div>
);
}
const placer = firstPlacers[0];
return (
<div className={styles.firstPlacers}>
<TournamentFirstPlacerWithMembers placer={placer} censored={censored} />
</div>
);
}
function TournamentFirstPlacerWithMembers({
placer,
censored,
}: {
placer: ShowcaseCalendarEvent["firstPlacers"][number];
censored: boolean;
}) {
const { t } = useTranslation(["front"]);
return (
<div className={styles.firstPlacers}>
<>
<div className="stack xs horizontal items-center text-xs">
{!censored && firstPlacer.logoUrl ? (
{!censored && placer.logoUrl ? (
<img
src={firstPlacer.logoUrl}
src={placer.logoUrl}
alt=""
width={24}
className="rounded-full"
@ -178,16 +213,16 @@ function TournamentFirstPlacers({
) : null}{" "}
<div className="stack items-start">
<span className={styles.firstPlacersTeamName}>
{censored ? "???" : firstPlacer.teamName}
{censored ? "???" : placer.teamName}
</span>
<div className="text-xxxs text-lighter font-bold text-uppercase">
{t("front:showcase.card.winner")}
{firstPlacer.div ? ` (${firstPlacer.div})` : null}
{placer.div ? ` (${placer.div})` : null}
</div>
</div>
</div>
<div className="text-xxs stack items-start mt-1">
{firstPlacer.members.map((member) => (
{placer.members.map((member) => (
<div key={member.id} className="stack horizontal xs items-center">
{!censored && member.country ? (
<Flag tiny countryCode={member.country} />
@ -195,12 +230,34 @@ function TournamentFirstPlacers({
{censored ? "???" : member.username}{" "}
</div>
))}
{!censored && firstPlacer.notShownMembersCount > 0 ? (
{!censored && placer.notShownMembersCount > 0 ? (
<div className="font-bold text-lighter">
+{firstPlacer.notShownMembersCount}
+{placer.notShownMembersCount}
</div>
) : null}
</div>
</>
);
}
function TournamentFirstPlacerTeamNameOnly({
placer,
censored,
}: {
placer: ShowcaseCalendarEvent["firstPlacers"][number];
censored: boolean;
}) {
const { t } = useTranslation(["front"]);
return (
<div className="stack items-start">
<span className={styles.firstPlacersTeamName}>
{censored ? "???" : placer.teamName}
</span>
<div className="text-xxxs text-lighter font-bold text-uppercase">
{t("front:showcase.card.winner")}
{placer.div ? ` (${placer.div})` : null}
</div>
</div>
);
}

View File

@ -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();