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