2025-11-12 23:44:33 -08:00
|
|
|
import { getAuth } from "@clerk/react-router/server";
|
|
|
|
|
import type { Route } from "./+types/$leagueId.sports-seasons.$sportsSeasonId";
|
|
|
|
|
import {
|
|
|
|
|
findLeagueById,
|
|
|
|
|
isUserLeagueMember,
|
|
|
|
|
isCommissioner,
|
|
|
|
|
findTeamsBySeasonId,
|
Optimize user data fetching with batch queries and centralize display name logic (#176)
* Fall back to displayName when username is null for Discord webhook
Users who sign up via OAuth (Google, GitHub, etc.) without setting a
Clerk username have a null `username` field but always have a `displayName`
(computed from firstName+lastName or email). Previously, `usernameByClerkId`
was filtered to only include users with a non-null username, causing those
owners to appear without any identifier in Discord standings messages
(e.g. "Liverpool def. Galatasaray" instead of "Liverpool def. Galatasaray (Madmike)").
https://claude.ai/code/session_01VAkeDDVZMYS1DweQnUrRnH
* Extract getUserDisplayName helper and use consistently throughout
Add a single getUserDisplayName(user) function to app/models/user.ts that
encapsulates the username → displayName fallback logic. Replace 9 scattered
inline expressions across the codebase (owner-map, scoring-calculator,
league routes, settings, invite flow, draft API, Clerk webhook) with calls
to the shared helper.
No behaviour change — all existing logic preserved, just centralised.
https://claude.ai/code/session_01VAkeDDVZMYS1DweQnUrRnH
* Fix N+1 user queries in league loader and settings loader
Add findUsersByClerkIds() batch function to the user model and replace two
separate Promise.all+findUserByClerkId loops (one for owners, one for
commissioners) with a single inArray query in both $leagueId.server.ts and
$leagueId.settings.tsx. The merged query covers both owner and commissioner
IDs in one round-trip.
https://claude.ai/code/session_01VAkeDDVZMYS1DweQnUrRnH
* Fix N+1 user queries in buildOwnerMap
Replace the Promise.all+findUserByClerkId loop with a single
findUsersByClerkIds batch query, consistent with the league loader
and settings loader fixes.
https://claude.ai/code/session_01VAkeDDVZMYS1DweQnUrRnH
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-03-18 16:46:07 -07:00
|
|
|
getUserDisplayName,
|
2025-11-12 23:44:33 -08:00
|
|
|
} from "~/models";
|
|
|
|
|
import { getDraftPicks } from "~/models/draft-pick";
|
|
|
|
|
import { getSeasonResults } from "~/models/participant-season-result";
|
2026-03-10 10:27:58 -07:00
|
|
|
import { calculateBracketPoints } from "~/models/scoring-rules";
|
2025-11-12 23:44:33 -08:00
|
|
|
import { getQPStandings } from "~/models/qualifying-points";
|
2026-03-07 21:59:29 -08:00
|
|
|
import {
|
|
|
|
|
getUpcomingScoringEvents,
|
|
|
|
|
getRecentCompletedEvents,
|
|
|
|
|
} from "~/models/scoring-event";
|
2026-03-21 00:12:01 -07:00
|
|
|
import { getRegularSeasonStandings } from "~/models/regular-season-standings";
|
|
|
|
|
import { getAllParticipantEVsForSeason } from "~/models/participant-expected-value";
|
2026-03-16 12:27:43 -07:00
|
|
|
import { getBracketTemplate, getOrderedRoundsFromMatches } from "~/lib/bracket-templates";
|
2026-03-21 09:44:05 -07:00
|
|
|
import type { PlayoffMatch } from "~/models/playoff-match";
|
2025-11-12 23:44:33 -08:00
|
|
|
import { database } from "~/database/context";
|
|
|
|
|
import * as schema from "~/database/schema";
|
2026-03-10 10:27:58 -07:00
|
|
|
import { eq, and, inArray } from "drizzle-orm";
|
2025-11-12 23:44:33 -08:00
|
|
|
|
|
|
|
|
export async function loader(args: Route.LoaderArgs) {
|
|
|
|
|
const { userId } = await getAuth(args);
|
|
|
|
|
const { params } = args;
|
|
|
|
|
const { leagueId, sportsSeasonId } = params;
|
|
|
|
|
|
|
|
|
|
// Check authentication
|
|
|
|
|
if (!userId) {
|
|
|
|
|
throw new Response("You must be logged in to view this page", {
|
|
|
|
|
status: 401,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Fetch league
|
|
|
|
|
const league = await findLeagueById(leagueId);
|
|
|
|
|
if (!league) {
|
|
|
|
|
throw new Response("League not found", { status: 404 });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check access: user must be a commissioner or member
|
|
|
|
|
const isUserCommissioner = await isCommissioner(leagueId, userId);
|
|
|
|
|
const isUserMember = await isUserLeagueMember(leagueId, userId);
|
|
|
|
|
|
|
|
|
|
if (!isUserCommissioner && !isUserMember) {
|
|
|
|
|
throw new Response("You do not have access to this league", {
|
|
|
|
|
status: 403,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Fetch sports season with relations
|
|
|
|
|
const db = database();
|
|
|
|
|
const sportsSeason = await db.query.sportsSeasons.findFirst({
|
|
|
|
|
where: eq(schema.sportsSeasons.id, sportsSeasonId),
|
|
|
|
|
with: {
|
|
|
|
|
sport: true,
|
|
|
|
|
seasonSports: {
|
|
|
|
|
with: {
|
|
|
|
|
season: true,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!sportsSeason) {
|
|
|
|
|
throw new Response("Sports season not found", { status: 404 });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get the fantasy season for this league (to get teams and draft picks)
|
|
|
|
|
// We need to find which season in this league includes this sports season
|
2026-02-20 11:35:35 -08:00
|
|
|
// Filter by leagueId since a sports season can be linked to multiple leagues
|
|
|
|
|
const seasonSport = sportsSeason.seasonSports?.find(
|
|
|
|
|
(ss) => ss.season.leagueId === leagueId
|
|
|
|
|
);
|
2025-11-12 23:44:33 -08:00
|
|
|
if (!seasonSport) {
|
|
|
|
|
throw new Response("This sports season is not linked to this league", {
|
|
|
|
|
status: 404,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-20 11:35:35 -08:00
|
|
|
const season = seasonSport.season;
|
2025-11-12 23:44:33 -08:00
|
|
|
|
|
|
|
|
// Fetch teams and draft picks to determine ownership
|
|
|
|
|
const teams = await findTeamsBySeasonId(season.id);
|
|
|
|
|
const draftPicks = await getDraftPicks(season.id);
|
|
|
|
|
|
|
|
|
|
// Build ownership map: participantId -> { teamName, teamId, ownerName }
|
|
|
|
|
const ownershipMap = new Map<
|
|
|
|
|
string,
|
|
|
|
|
{ teamName: string; teamId: string; ownerName?: string }
|
|
|
|
|
>();
|
|
|
|
|
|
2026-03-10 10:27:58 -07:00
|
|
|
// Get unique owner IDs and batch-fetch their user records in a single query
|
|
|
|
|
const ownerIds = [...new Set(teams.map((t) => t.ownerId).filter(Boolean))] as string[];
|
|
|
|
|
const ownerUserRows = ownerIds.length > 0
|
|
|
|
|
? await db.query.users.findMany({
|
|
|
|
|
where: inArray(schema.users.clerkId, ownerIds),
|
|
|
|
|
columns: { clerkId: true, username: true, displayName: true },
|
|
|
|
|
})
|
|
|
|
|
: [];
|
|
|
|
|
const ownerMap = new Map(
|
Optimize user data fetching with batch queries and centralize display name logic (#176)
* Fall back to displayName when username is null for Discord webhook
Users who sign up via OAuth (Google, GitHub, etc.) without setting a
Clerk username have a null `username` field but always have a `displayName`
(computed from firstName+lastName or email). Previously, `usernameByClerkId`
was filtered to only include users with a non-null username, causing those
owners to appear without any identifier in Discord standings messages
(e.g. "Liverpool def. Galatasaray" instead of "Liverpool def. Galatasaray (Madmike)").
https://claude.ai/code/session_01VAkeDDVZMYS1DweQnUrRnH
* Extract getUserDisplayName helper and use consistently throughout
Add a single getUserDisplayName(user) function to app/models/user.ts that
encapsulates the username → displayName fallback logic. Replace 9 scattered
inline expressions across the codebase (owner-map, scoring-calculator,
league routes, settings, invite flow, draft API, Clerk webhook) with calls
to the shared helper.
No behaviour change — all existing logic preserved, just centralised.
https://claude.ai/code/session_01VAkeDDVZMYS1DweQnUrRnH
* Fix N+1 user queries in league loader and settings loader
Add findUsersByClerkIds() batch function to the user model and replace two
separate Promise.all+findUserByClerkId loops (one for owners, one for
commissioners) with a single inArray query in both $leagueId.server.ts and
$leagueId.settings.tsx. The merged query covers both owner and commissioner
IDs in one round-trip.
https://claude.ai/code/session_01VAkeDDVZMYS1DweQnUrRnH
* Fix N+1 user queries in buildOwnerMap
Replace the Promise.all+findUserByClerkId loop with a single
findUsersByClerkIds batch query, consistent with the league loader
and settings loader fixes.
https://claude.ai/code/session_01VAkeDDVZMYS1DweQnUrRnH
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-03-18 16:46:07 -07:00
|
|
|
ownerUserRows.map((u) => [u.clerkId, getUserDisplayName(u) ?? "Unknown"])
|
2025-11-12 23:44:33 -08:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Map draft picks to ownership
|
|
|
|
|
for (const pick of draftPicks) {
|
|
|
|
|
const team = teams.find((t) => t.id === pick.teamId);
|
|
|
|
|
if (team && pick.participantId) {
|
|
|
|
|
ownershipMap.set(pick.participantId, {
|
|
|
|
|
teamName: team.name,
|
|
|
|
|
teamId: team.id,
|
|
|
|
|
ownerName: team.ownerId ? ownerMap.get(team.ownerId) : undefined,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Convert to array for serialization
|
|
|
|
|
const teamOwnerships = Array.from(ownershipMap.entries()).map(
|
|
|
|
|
([participantId, data]) => ({
|
|
|
|
|
participantId,
|
|
|
|
|
...data,
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
|
2026-03-07 21:59:29 -08:00
|
|
|
// Derive which participant IDs the current user has drafted
|
|
|
|
|
const userTeams = teams.filter((t) => t.ownerId === userId);
|
|
|
|
|
const userTeamIds = new Set(userTeams.map((t) => t.id));
|
|
|
|
|
const userParticipantIds = draftPicks
|
|
|
|
|
.filter((p) => p.teamId && userTeamIds.has(p.teamId) && p.participantId)
|
|
|
|
|
.map((p) => p.participantId as string);
|
|
|
|
|
|
2025-11-12 23:44:33 -08:00
|
|
|
// Fetch pattern-specific data based on scoring pattern
|
|
|
|
|
const scoringPattern = sportsSeason.scoringPattern;
|
|
|
|
|
|
2026-03-07 21:59:29 -08:00
|
|
|
type SeasonStanding = { id: string; position: number; championshipPoints: string; participant: { id: string; name: string } };
|
|
|
|
|
|
2026-03-21 09:44:05 -07:00
|
|
|
type PlayoffMatchWithRelations = PlayoffMatch & {
|
|
|
|
|
participant1: { id: string; name: string } | null;
|
|
|
|
|
participant2: { id: string; name: string } | null;
|
|
|
|
|
winner: { id: string; name: string } | null;
|
|
|
|
|
loser: { id: string; name: string } | null;
|
|
|
|
|
};
|
|
|
|
|
let playoffMatches: PlayoffMatchWithRelations[] = [];
|
2025-11-12 23:44:33 -08:00
|
|
|
let playoffRounds: string[] = [];
|
User/chris/bracket UI redesign (#95)
* feat: redesign playoff bracket UI with compact layout and owner display
- Replace per-match Card wrappers with compact left-vs-right rows (stacks on mobile)
- Add TeamOwnerBadge component (colored avatar + team name + username), matching the standings "Drafted By" style
- Show owner info below each participant name in bracket matches
- Add TBD forward-reference labels ("Winner of Quarterfinals M1") computed via buildFeederMap
- Add Final Rankings table below bracket showing all participants ranked by elimination round
- Fix round ordering in loader: sort by match count descending (more matches = earlier round)
- Add loser relation to playoff matches query for elimination tracking
- Extract getAvatarColor to app/lib/color-hash.ts (shared across GroupStageDisplay, SeasonStandings, TeamOwnerBadge)
- Extract groupMatchesByRound helper; share grouped map between feeder computation and render
- Remove dead getTeamAvatar from SeasonStandings after TeamOwnerBadge adoption
- Add tests for groupMatchesByRound and buildFeederMap (7 tests)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: add draft order card to league home page
Show draft order on the league home page when order is set and season
is in pre_draft or draft status. Includes team name, owner name, and
a link to the draft room.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: enhance playoff bracket with eliminated teams, points, and ownership highlights
- Show eliminated teams (including group-stage losers) in Final Rankings card
- Display computed fantasy points per participant in rankings table
- Card title switches between "Eliminated Teams" and "Final Rankings" based on bracket completion
- Highlight owned participants with electric blue name, dot indicator, and card border in bracket matches
- Highlight owned rows with electric border/background in rankings table
- Suppress EventSchedule for playoff_bracket sports seasons
- Fix title redundancy: bracket section title no longer repeats sport name
- Two-column match grid on desktop (md:grid-cols-2)
- Code review fixes: parallel DB fetches, targeted query for pre-eliminated, single-pass parseFloat, remove IIFE
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 22:44:33 -07:00
|
|
|
let preEliminatedParticipants: { id: string; name: string }[] = [];
|
|
|
|
|
let participantPoints: { participantId: string; points: number }[] = [];
|
2026-03-10 10:27:58 -07:00
|
|
|
let partialScoreParticipantIds: string[] = [];
|
2026-03-07 21:59:29 -08:00
|
|
|
let seasonStandings: SeasonStanding[] = [];
|
2026-03-21 09:44:05 -07:00
|
|
|
type QPStanding = Awaited<ReturnType<typeof getQPStandings>>[number];
|
|
|
|
|
let qpStandings: QPStanding[] = [];
|
2025-11-12 23:44:33 -08:00
|
|
|
|
2026-03-07 21:59:29 -08:00
|
|
|
if (scoringPattern === "playoff_bracket") {
|
2025-11-12 23:44:33 -08:00
|
|
|
// Fetch playoff matches via scoring events
|
|
|
|
|
const events = await db.query.scoringEvents.findMany({
|
|
|
|
|
where: eq(schema.scoringEvents.sportsSeasonId, sportsSeasonId),
|
|
|
|
|
});
|
|
|
|
|
|
2026-03-17 12:34:10 -07:00
|
|
|
let templateId: string | undefined;
|
|
|
|
|
|
2025-11-12 23:44:33 -08:00
|
|
|
if (events.length > 0) {
|
|
|
|
|
const eventIds = events.map((e) => e.id);
|
|
|
|
|
const matches = await db.query.playoffMatches.findMany({
|
|
|
|
|
where: (playoffMatches, { inArray }) =>
|
|
|
|
|
inArray(playoffMatches.scoringEventId, eventIds),
|
|
|
|
|
with: {
|
|
|
|
|
participant1: true,
|
|
|
|
|
participant2: true,
|
|
|
|
|
winner: true,
|
User/chris/bracket UI redesign (#95)
* feat: redesign playoff bracket UI with compact layout and owner display
- Replace per-match Card wrappers with compact left-vs-right rows (stacks on mobile)
- Add TeamOwnerBadge component (colored avatar + team name + username), matching the standings "Drafted By" style
- Show owner info below each participant name in bracket matches
- Add TBD forward-reference labels ("Winner of Quarterfinals M1") computed via buildFeederMap
- Add Final Rankings table below bracket showing all participants ranked by elimination round
- Fix round ordering in loader: sort by match count descending (more matches = earlier round)
- Add loser relation to playoff matches query for elimination tracking
- Extract getAvatarColor to app/lib/color-hash.ts (shared across GroupStageDisplay, SeasonStandings, TeamOwnerBadge)
- Extract groupMatchesByRound helper; share grouped map between feeder computation and render
- Remove dead getTeamAvatar from SeasonStandings after TeamOwnerBadge adoption
- Add tests for groupMatchesByRound and buildFeederMap (7 tests)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: add draft order card to league home page
Show draft order on the league home page when order is set and season
is in pre_draft or draft status. Includes team name, owner name, and
a link to the draft room.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: enhance playoff bracket with eliminated teams, points, and ownership highlights
- Show eliminated teams (including group-stage losers) in Final Rankings card
- Display computed fantasy points per participant in rankings table
- Card title switches between "Eliminated Teams" and "Final Rankings" based on bracket completion
- Highlight owned participants with electric blue name, dot indicator, and card border in bracket matches
- Highlight owned rows with electric border/background in rankings table
- Suppress EventSchedule for playoff_bracket sports seasons
- Fix title redundancy: bracket section title no longer repeats sport name
- Two-column match grid on desktop (md:grid-cols-2)
- Code review fixes: parallel DB fetches, targeted query for pre-eliminated, single-pass parseFloat, remove IIFE
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 22:44:33 -07:00
|
|
|
loser: true,
|
2025-11-12 23:44:33 -08:00
|
|
|
},
|
|
|
|
|
});
|
2026-03-21 09:44:05 -07:00
|
|
|
playoffMatches = matches as PlayoffMatchWithRelations[];
|
2025-11-12 23:44:33 -08:00
|
|
|
|
2026-03-17 12:34:10 -07:00
|
|
|
templateId = events.find((e) => e.bracketTemplateId)?.bracketTemplateId ?? undefined;
|
2026-03-16 12:27:43 -07:00
|
|
|
const template = templateId ? getBracketTemplate(templateId) : undefined;
|
|
|
|
|
playoffRounds = getOrderedRoundsFromMatches(matches, template);
|
2025-11-12 23:44:33 -08:00
|
|
|
}
|
User/chris/bracket UI redesign (#95)
* feat: redesign playoff bracket UI with compact layout and owner display
- Replace per-match Card wrappers with compact left-vs-right rows (stacks on mobile)
- Add TeamOwnerBadge component (colored avatar + team name + username), matching the standings "Drafted By" style
- Show owner info below each participant name in bracket matches
- Add TBD forward-reference labels ("Winner of Quarterfinals M1") computed via buildFeederMap
- Add Final Rankings table below bracket showing all participants ranked by elimination round
- Fix round ordering in loader: sort by match count descending (more matches = earlier round)
- Add loser relation to playoff matches query for elimination tracking
- Extract getAvatarColor to app/lib/color-hash.ts (shared across GroupStageDisplay, SeasonStandings, TeamOwnerBadge)
- Extract groupMatchesByRound helper; share grouped map between feeder computation and render
- Remove dead getTeamAvatar from SeasonStandings after TeamOwnerBadge adoption
- Add tests for groupMatchesByRound and buildFeederMap (7 tests)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: add draft order card to league home page
Show draft order on the league home page when order is set and season
is in pre_draft or draft status. Includes team name, owner name, and
a link to the draft room.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: enhance playoff bracket with eliminated teams, points, and ownership highlights
- Show eliminated teams (including group-stage losers) in Final Rankings card
- Display computed fantasy points per participant in rankings table
- Card title switches between "Eliminated Teams" and "Final Rankings" based on bracket completion
- Highlight owned participants with electric blue name, dot indicator, and card border in bracket matches
- Highlight owned rows with electric border/background in rankings table
- Suppress EventSchedule for playoff_bracket sports seasons
- Fix title redundancy: bracket section title no longer repeats sport name
- Two-column match grid on desktop (md:grid-cols-2)
- Code review fixes: parallel DB fetches, targeted query for pre-eliminated, single-pass parseFloat, remove IIFE
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 22:44:33 -07:00
|
|
|
|
2026-03-10 10:27:58 -07:00
|
|
|
// Fetch group-stage losers and all participant results for bracket scoring
|
|
|
|
|
const [eliminatedResults, allResults] = await Promise.all([
|
User/chris/bracket UI redesign (#95)
* feat: redesign playoff bracket UI with compact layout and owner display
- Replace per-match Card wrappers with compact left-vs-right rows (stacks on mobile)
- Add TeamOwnerBadge component (colored avatar + team name + username), matching the standings "Drafted By" style
- Show owner info below each participant name in bracket matches
- Add TBD forward-reference labels ("Winner of Quarterfinals M1") computed via buildFeederMap
- Add Final Rankings table below bracket showing all participants ranked by elimination round
- Fix round ordering in loader: sort by match count descending (more matches = earlier round)
- Add loser relation to playoff matches query for elimination tracking
- Extract getAvatarColor to app/lib/color-hash.ts (shared across GroupStageDisplay, SeasonStandings, TeamOwnerBadge)
- Extract groupMatchesByRound helper; share grouped map between feeder computation and render
- Remove dead getTeamAvatar from SeasonStandings after TeamOwnerBadge adoption
- Add tests for groupMatchesByRound and buildFeederMap (7 tests)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: add draft order card to league home page
Show draft order on the league home page when order is set and season
is in pre_draft or draft status. Includes team name, owner name, and
a link to the draft room.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: enhance playoff bracket with eliminated teams, points, and ownership highlights
- Show eliminated teams (including group-stage losers) in Final Rankings card
- Display computed fantasy points per participant in rankings table
- Card title switches between "Eliminated Teams" and "Final Rankings" based on bracket completion
- Highlight owned participants with electric blue name, dot indicator, and card border in bracket matches
- Highlight owned rows with electric border/background in rankings table
- Suppress EventSchedule for playoff_bracket sports seasons
- Fix title redundancy: bracket section title no longer repeats sport name
- Two-column match grid on desktop (md:grid-cols-2)
- Code review fixes: parallel DB fetches, targeted query for pre-eliminated, single-pass parseFloat, remove IIFE
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 22:44:33 -07:00
|
|
|
db.query.participantResults.findMany({
|
|
|
|
|
where: and(
|
|
|
|
|
eq(schema.participantResults.sportsSeasonId, sportsSeasonId),
|
|
|
|
|
eq(schema.participantResults.finalPosition, 0)
|
|
|
|
|
),
|
|
|
|
|
with: { participant: true },
|
|
|
|
|
}),
|
2026-03-10 10:27:58 -07:00
|
|
|
db.query.participantResults.findMany({
|
|
|
|
|
where: eq(schema.participantResults.sportsSeasonId, sportsSeasonId),
|
|
|
|
|
}),
|
User/chris/bracket UI redesign (#95)
* feat: redesign playoff bracket UI with compact layout and owner display
- Replace per-match Card wrappers with compact left-vs-right rows (stacks on mobile)
- Add TeamOwnerBadge component (colored avatar + team name + username), matching the standings "Drafted By" style
- Show owner info below each participant name in bracket matches
- Add TBD forward-reference labels ("Winner of Quarterfinals M1") computed via buildFeederMap
- Add Final Rankings table below bracket showing all participants ranked by elimination round
- Fix round ordering in loader: sort by match count descending (more matches = earlier round)
- Add loser relation to playoff matches query for elimination tracking
- Extract getAvatarColor to app/lib/color-hash.ts (shared across GroupStageDisplay, SeasonStandings, TeamOwnerBadge)
- Extract groupMatchesByRound helper; share grouped map between feeder computation and render
- Remove dead getTeamAvatar from SeasonStandings after TeamOwnerBadge adoption
- Add tests for groupMatchesByRound and buildFeederMap (7 tests)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: add draft order card to league home page
Show draft order on the league home page when order is set and season
is in pre_draft or draft status. Includes team name, owner name, and
a link to the draft room.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: enhance playoff bracket with eliminated teams, points, and ownership highlights
- Show eliminated teams (including group-stage losers) in Final Rankings card
- Display computed fantasy points per participant in rankings table
- Card title switches between "Eliminated Teams" and "Final Rankings" based on bracket completion
- Highlight owned participants with electric blue name, dot indicator, and card border in bracket matches
- Highlight owned rows with electric border/background in rankings table
- Suppress EventSchedule for playoff_bracket sports seasons
- Fix title redundancy: bracket section title no longer repeats sport name
- Two-column match grid on desktop (md:grid-cols-2)
- Code review fixes: parallel DB fetches, targeted query for pre-eliminated, single-pass parseFloat, remove IIFE
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 22:44:33 -07:00
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
preEliminatedParticipants = eliminatedResults
|
2026-03-21 09:44:05 -07:00
|
|
|
.filter((r): r is typeof r & { participant: NonNullable<typeof r.participant> } => r.participant !== null && r.participant !== undefined)
|
|
|
|
|
.map((r) => ({ id: r.participant.id, name: r.participant.name }));
|
User/chris/bracket UI redesign (#95)
* feat: redesign playoff bracket UI with compact layout and owner display
- Replace per-match Card wrappers with compact left-vs-right rows (stacks on mobile)
- Add TeamOwnerBadge component (colored avatar + team name + username), matching the standings "Drafted By" style
- Show owner info below each participant name in bracket matches
- Add TBD forward-reference labels ("Winner of Quarterfinals M1") computed via buildFeederMap
- Add Final Rankings table below bracket showing all participants ranked by elimination round
- Fix round ordering in loader: sort by match count descending (more matches = earlier round)
- Add loser relation to playoff matches query for elimination tracking
- Extract getAvatarColor to app/lib/color-hash.ts (shared across GroupStageDisplay, SeasonStandings, TeamOwnerBadge)
- Extract groupMatchesByRound helper; share grouped map between feeder computation and render
- Remove dead getTeamAvatar from SeasonStandings after TeamOwnerBadge adoption
- Add tests for groupMatchesByRound and buildFeederMap (7 tests)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: add draft order card to league home page
Show draft order on the league home page when order is set and season
is in pre_draft or draft status. Includes team name, owner name, and
a link to the draft room.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: enhance playoff bracket with eliminated teams, points, and ownership highlights
- Show eliminated teams (including group-stage losers) in Final Rankings card
- Display computed fantasy points per participant in rankings table
- Card title switches between "Eliminated Teams" and "Final Rankings" based on bracket completion
- Highlight owned participants with electric blue name, dot indicator, and card border in bracket matches
- Highlight owned rows with electric border/background in rankings table
- Suppress EventSchedule for playoff_bracket sports seasons
- Fix title redundancy: bracket section title no longer repeats sport name
- Two-column match grid on desktop (md:grid-cols-2)
- Code review fixes: parallel DB fetches, targeted query for pre-eliminated, single-pass parseFloat, remove IIFE
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 22:44:33 -07:00
|
|
|
|
2026-03-10 10:27:58 -07:00
|
|
|
// Compute per-participant fantasy points directly from participant_results.
|
|
|
|
|
// This covers both finalized placements and progressive floor scores for still-alive participants.
|
|
|
|
|
const scoringRules = {
|
|
|
|
|
pointsFor1st: season.pointsFor1st,
|
|
|
|
|
pointsFor2nd: season.pointsFor2nd,
|
|
|
|
|
pointsFor3rd: season.pointsFor3rd,
|
|
|
|
|
pointsFor4th: season.pointsFor4th,
|
|
|
|
|
pointsFor5th: season.pointsFor5th,
|
|
|
|
|
pointsFor6th: season.pointsFor6th,
|
|
|
|
|
pointsFor7th: season.pointsFor7th,
|
|
|
|
|
pointsFor8th: season.pointsFor8th,
|
|
|
|
|
};
|
|
|
|
|
const seenParticipantIds = new Set<string>();
|
|
|
|
|
participantPoints = allResults
|
|
|
|
|
.filter((r) => r.finalPosition !== null && r.finalPosition > 0)
|
|
|
|
|
.filter((r) => {
|
|
|
|
|
if (seenParticipantIds.has(r.participantId)) return false;
|
|
|
|
|
seenParticipantIds.add(r.participantId);
|
|
|
|
|
return true;
|
|
|
|
|
})
|
|
|
|
|
.map((r) => ({
|
|
|
|
|
participantId: r.participantId,
|
2026-03-17 12:34:10 -07:00
|
|
|
points: calculateBracketPoints(r.finalPosition!, scoringRules, templateId),
|
2026-03-10 10:27:58 -07:00
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
// Track which participants have provisional (floor) scores — still competing
|
|
|
|
|
partialScoreParticipantIds = allResults
|
|
|
|
|
.filter((r) => r.isPartialScore)
|
|
|
|
|
.map((r) => r.participantId);
|
2025-11-12 23:44:33 -08:00
|
|
|
} else if (scoringPattern === "season_standings") {
|
2026-03-07 21:59:29 -08:00
|
|
|
// Fetch F1-style championship standings and map to SeasonStanding shape
|
2025-11-12 23:44:33 -08:00
|
|
|
const results = await getSeasonResults(sportsSeasonId);
|
2026-03-07 21:59:29 -08:00
|
|
|
seasonStandings = results
|
|
|
|
|
.filter((r) => r.currentPosition !== null || (r.currentPoints && parseFloat(r.currentPoints) > 0))
|
|
|
|
|
.map((r) => ({
|
|
|
|
|
id: r.id,
|
|
|
|
|
position: r.currentPosition ?? 999,
|
|
|
|
|
championshipPoints: r.currentPoints ?? "0",
|
|
|
|
|
participant: {
|
|
|
|
|
id: r.participant.id,
|
|
|
|
|
name: r.participant.name,
|
|
|
|
|
},
|
|
|
|
|
}));
|
2025-11-12 23:44:33 -08:00
|
|
|
} else if (scoringPattern === "qualifying_points") {
|
|
|
|
|
// Fetch qualifying points standings
|
|
|
|
|
const standings = await getQPStandings(sportsSeasonId);
|
|
|
|
|
qpStandings = standings;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-07 21:59:29 -08:00
|
|
|
// Fetch event schedule for all patterns (upcoming + recent)
|
|
|
|
|
const [upcomingEvents, recentEvents] = await Promise.all([
|
|
|
|
|
getUpcomingScoringEvents(sportsSeasonId, 5),
|
|
|
|
|
getRecentCompletedEvents(sportsSeasonId, 3),
|
|
|
|
|
]);
|
|
|
|
|
|
2026-03-21 00:12:01 -07:00
|
|
|
// Fetch regular season standings for team-sport playoff_bracket seasons (NBA, NHL)
|
|
|
|
|
const isTeamBracket =
|
|
|
|
|
scoringPattern === "playoff_bracket" && sportsSeason.sport?.type === "team";
|
|
|
|
|
const [regularSeasonStandings, regularSeasonEvs] = isTeamBracket
|
|
|
|
|
? await Promise.all([
|
|
|
|
|
getRegularSeasonStandings(sportsSeasonId),
|
|
|
|
|
getAllParticipantEVsForSeason(sportsSeasonId),
|
|
|
|
|
])
|
|
|
|
|
: [[], []];
|
|
|
|
|
|
2026-03-07 21:59:29 -08:00
|
|
|
// Derive seasonIsFinalized from status
|
|
|
|
|
const seasonIsFinalized = sportsSeason.status === "completed";
|
|
|
|
|
|
2026-03-21 00:12:01 -07:00
|
|
|
// Build participantId → expectedValue map for display
|
|
|
|
|
const participantEvs = Object.fromEntries(
|
|
|
|
|
regularSeasonEvs.map((ev) => [ev.participantId, ev.expectedValue])
|
|
|
|
|
);
|
|
|
|
|
|
2025-11-12 23:44:33 -08:00
|
|
|
return {
|
|
|
|
|
league,
|
|
|
|
|
season,
|
|
|
|
|
sportsSeason,
|
|
|
|
|
scoringPattern,
|
|
|
|
|
playoffMatches,
|
|
|
|
|
playoffRounds,
|
User/chris/bracket UI redesign (#95)
* feat: redesign playoff bracket UI with compact layout and owner display
- Replace per-match Card wrappers with compact left-vs-right rows (stacks on mobile)
- Add TeamOwnerBadge component (colored avatar + team name + username), matching the standings "Drafted By" style
- Show owner info below each participant name in bracket matches
- Add TBD forward-reference labels ("Winner of Quarterfinals M1") computed via buildFeederMap
- Add Final Rankings table below bracket showing all participants ranked by elimination round
- Fix round ordering in loader: sort by match count descending (more matches = earlier round)
- Add loser relation to playoff matches query for elimination tracking
- Extract getAvatarColor to app/lib/color-hash.ts (shared across GroupStageDisplay, SeasonStandings, TeamOwnerBadge)
- Extract groupMatchesByRound helper; share grouped map between feeder computation and render
- Remove dead getTeamAvatar from SeasonStandings after TeamOwnerBadge adoption
- Add tests for groupMatchesByRound and buildFeederMap (7 tests)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: add draft order card to league home page
Show draft order on the league home page when order is set and season
is in pre_draft or draft status. Includes team name, owner name, and
a link to the draft room.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: enhance playoff bracket with eliminated teams, points, and ownership highlights
- Show eliminated teams (including group-stage losers) in Final Rankings card
- Display computed fantasy points per participant in rankings table
- Card title switches between "Eliminated Teams" and "Final Rankings" based on bracket completion
- Highlight owned participants with electric blue name, dot indicator, and card border in bracket matches
- Highlight owned rows with electric border/background in rankings table
- Suppress EventSchedule for playoff_bracket sports seasons
- Fix title redundancy: bracket section title no longer repeats sport name
- Two-column match grid on desktop (md:grid-cols-2)
- Code review fixes: parallel DB fetches, targeted query for pre-eliminated, single-pass parseFloat, remove IIFE
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 22:44:33 -07:00
|
|
|
preEliminatedParticipants,
|
|
|
|
|
participantPoints,
|
2026-03-10 10:27:58 -07:00
|
|
|
partialScoreParticipantIds,
|
2025-11-12 23:44:33 -08:00
|
|
|
seasonStandings,
|
|
|
|
|
qpStandings,
|
|
|
|
|
teamOwnerships,
|
2026-03-07 21:59:29 -08:00
|
|
|
userParticipantIds,
|
|
|
|
|
upcomingEvents,
|
|
|
|
|
recentEvents,
|
|
|
|
|
seasonIsFinalized,
|
2026-03-21 00:12:01 -07:00
|
|
|
regularSeasonStandings,
|
|
|
|
|
participantEvs,
|
2025-11-12 23:44:33 -08:00
|
|
|
};
|
|
|
|
|
}
|