Show full matchup in My Upcoming Events for bracket matches

Bracket match matchLabel now includes "P1 vs P2" alongside the round
name (e.g. "Quarterfinals — Team Spirit vs G2 Esports") so users can
see who their drafted team is playing, not just the team they own.
relevantParticipants stays drafted-only since it also drives the
"X of your picks" badge UI.

https://claude.ai/code/session_01NpQNGWacjg7mbnq8aaUweJ
This commit is contained in:
Claude 2026-06-16 19:43:19 +00:00
parent fe4e1b3f3c
commit f91f7dd7ad
No known key found for this signature in database
2 changed files with 110 additions and 3 deletions

View file

@ -459,6 +459,83 @@ describe("getUpcomingEventsForDraftedParticipants — bracket sports", () => {
expect(result[0].matchLabel).toBeNull();
});
it("includes the P1 vs P2 matchup in matchLabel alongside the round name", async () => {
const rows = [{
id: "e1", name: "IEM Cologne 2026", eventDate: "2026-06-19",
eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID,
playoffMatchId: "pm1",
participant1Id: "team-spirit", participant2Id: "g2-esports",
round: "Quarterfinals", gameNumber: 1, scheduledAt: null,
}];
mockDb.selectDistinct.mockReturnValueOnce(makeSelectChain(rows));
mockDb.query.seasonParticipants.findMany.mockResolvedValueOnce([
{ id: "team-spirit", name: "Team Spirit" },
{ id: "g2-esports", name: "G2 Esports" },
]);
const result = await getUpcomingEventsForDraftedParticipants(
SPORTS_SEASON_ID, "playoff_bracket",
[makeParticipant("g2-esports", "G2 Esports")],
DATE_FROM, DATE_TO
);
expect(result[0].matchLabel).toBe("Quarterfinals — Team Spirit vs G2 Esports");
// relevantParticipants stays drafted-only — the opponent is only surfaced via matchLabel.
expect(result[0].relevantParticipants).toEqual([{ id: "g2-esports", name: "G2 Esports" }]);
});
it("shows just the matchup (no round prefix) when round name is redundant with event name", async () => {
const rows = [{
id: "e1", name: "Knockout Stage", eventDate: null,
eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID,
playoffMatchId: "pm1",
participant1Id: "barcelona", participant2Id: "sporting",
round: "Knockout Stage", gameNumber: 1, scheduledAt: null,
}];
mockDb.selectDistinct.mockReturnValueOnce(makeSelectChain(rows));
mockDb.query.seasonParticipants.findMany.mockResolvedValueOnce([
{ id: "barcelona", name: "Barcelona" },
{ id: "sporting", name: "Sporting" },
]);
const result = await getUpcomingEventsForDraftedParticipants(
SPORTS_SEASON_ID, "playoff_bracket",
[makeParticipant("barcelona", "Barcelona")],
DATE_FROM, DATE_TO
);
expect(result[0].matchLabel).toBe("Barcelona vs Sporting");
});
it("includes the matchup in matchLabel for series games", async () => {
const gameTime1 = new Date("2025-04-09T13:00:00.000Z");
const rows = [{
id: "e1", name: "UCL QF", eventDate: null,
eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID,
playoffMatchId: "pm1",
participant1Id: "barcelona", participant2Id: "sporting",
round: "Knockout Stage", gameNumber: 1, scheduledAt: gameTime1,
}];
mockDb.selectDistinct
.mockReturnValueOnce(makeSelectChain(rows))
.mockReturnValueOnce(makeMaxGameChain([
{ eventId: "e1", gameNumber: 1 },
{ eventId: "e1", gameNumber: 2 },
]));
mockDb.query.seasonParticipants.findMany.mockResolvedValueOnce([
{ id: "barcelona", name: "Barcelona" },
{ id: "sporting", name: "Sporting" },
]);
const result = await getUpcomingEventsForDraftedParticipants(
SPORTS_SEASON_ID, "playoff_bracket",
[makeParticipant("barcelona", "Barcelona")],
DATE_FROM, DATE_TO
);
expect(result[0].matchLabel).toBe("Knockout Stage Game #1 — Barcelona vs Sporting");
});
it("shows game number when only game #2 is in the window (game #1 already played)", async () => {
const rows = [{
id: "e1", name: "PDC World Championship", eventDate: "2025-04-10",

View file

@ -607,6 +607,24 @@ export async function getUpcomingEventsForDraftedParticipants(
)
.orderBy(asc(schema.scoringEvents.eventDate));
// Look up names for ALL participants in these matches (not just drafted
// ones) so matchLabel can show the full "P1 vs P2" matchup, mirroring the
// group-stage matchLabel below.
const allMatchParticipantIds = [
...new Set(
rows.flatMap((r) => [r.participant1Id, r.participant2Id]).filter((id): id is string => !!id)
),
];
const matchParticipantNameById = new Map<string, string>();
if (allMatchParticipantIds.length > 0) {
const matchParticipantRows = await db.query.seasonParticipants.findMany({
where: inArray(schema.seasonParticipants.id, allMatchParticipantIds),
});
for (const p of matchParticipantRows) {
matchParticipantNameById.set(p.id, p.name);
}
}
// Determine max game number per event across ALL games (not date-filtered)
// so we can distinguish single-game matchups from multi-game series.
const eventIdsFromRows = [...new Set(rows.map((r) => r.id))];
@ -644,6 +662,7 @@ export async function getUpcomingEventsForDraftedParticipants(
const gameKeysByEntry = new Map<string, Set<string>>();
const earliestTimeByEntry = new Map<string, Date>();
const isSeriesByEntry = new Map<string, boolean>();
const matchupIdsByEntry = new Map<string, { p1Id: string | null; p2Id: string | null }>();
for (const row of rows) {
const isSeries = (maxGameNumberByEvent.get(row.id) ?? 1) > 1;
@ -667,6 +686,7 @@ export async function getUpcomingEventsForDraftedParticipants(
participantsByEntry.set(entryKey, new Map());
gameKeysByEntry.set(entryKey, new Set());
isSeriesByEntry.set(entryKey, isSeries);
matchupIdsByEntry.set(entryKey, { p1Id: row.participant1Id, p2Id: row.participant2Id });
}
const pMap = participantsByEntry.get(entryKey);
@ -702,25 +722,35 @@ export async function getUpcomingEventsForDraftedParticipants(
const isSeries = isSeriesByEntry.get(entryKey) ?? false;
const gameKeys = gameKeysByEntry.get(entryKey) ?? new Set<string>();
const matchupIds = matchupIdsByEntry.get(entryKey);
const p1Name = matchupIds?.p1Id ? matchParticipantNameById.get(matchupIds.p1Id) : undefined;
const p2Name = matchupIds?.p2Id ? matchParticipantNameById.get(matchupIds.p2Id) : undefined;
const matchupLabel = p1Name && p2Name ? `${p1Name} vs ${p2Name}` : null;
let roundLabel: string | null = null;
if (isSeries) {
if (gameKeys.size >= 1) {
const [round, gameNumberStr] = [...gameKeys][0].split("|");
event.matchLabel =
roundLabel =
round === event.name ? `Game #${gameNumberStr}` : `${round} Game #${gameNumberStr}`;
}
} else {
// Single-game: omit the game number, just show the round name.
if (gameKeys.size === 1) {
const [round] = [...gameKeys][0].split("|");
event.matchLabel = round === event.name ? null : round;
roundLabel = round === event.name ? null : round;
} else if (gameKeys.size > 1) {
const rounds = new Set([...gameKeys].map((k) => k.split("|")[0]));
if (rounds.size === 1) {
const round = [...rounds][0];
event.matchLabel = round === event.name ? null : round;
roundLabel = round === event.name ? null : round;
}
}
}
event.matchLabel = roundLabel && matchupLabel
? `${roundLabel}${matchupLabel}`
: roundLabel ?? matchupLabel;
}
const bracketResults = Array.from(entryMap.values());