From 063834d8e6130e1f7ad0996caa677a4313b46e77 Mon Sep 17 00:00:00 2001 From: Chris Parsons <438676+chrisparsons83@users.noreply.github.com> Date: Thu, 19 Mar 2026 20:19:28 -0700 Subject: [PATCH] Display team owner names in standings views (#184) * Show team name + username in standings, extract TeamNameDisplay component - Add TeamNameDisplay component that renders team name (as link) with owner username below, matching the league homepage style - Update StandingsTable to use TeamNameDisplay with owner username shown below team name - Update league homepage standings section to use TeamNameDisplay - Add ownerName/teamOwnerId fields to TeamStanding type - Extend getSeasonStandings to include teamOwnerId from team relation - Fetch and attach owner display names in the full standings page loader https://claude.ai/code/session_01EYgGnuTBaRVdBDapJRTxDZ * Address code review: type hygiene, explicit types, parallel fetching, style fix - Remove teamOwnerId from TeamStanding type (was an internal server concern, not display data) - Remove teamOwnerId from getSeasonStandings return value - Add TeamStandingWithChange interface extending TeamStanding with sevenDayRankChange/sevenDayOldRank fields - Annotate getSevenDayStandingsChange with explicit Promise return type - Re-export TeamStandingWithChange from models/standings.ts - Standings loader: query teams table directly for ownerIds instead of relying on teamOwnerId in standings data - Standings loader: parallelize all independent queries (standings, teams, progressionData, seasonComplete, completionPercentage) with Promise.all - Restore font-medium on StandingsTable team TableCell https://claude.ai/code/session_01EYgGnuTBaRVdBDapJRTxDZ --------- Co-authored-by: Claude --- app/components/standings/StandingsTable.tsx | 13 ++++--- app/components/ui/team-name-display.tsx | 31 ++++++++++++++++ app/models/standings.ts | 6 ++-- .../leagues/$leagueId.standings.$seasonId.tsx | 36 ++++++++++++++----- app/routes/leagues/$leagueId.tsx | 17 ++++----- app/types/standings.ts | 6 ++++ 6 files changed, 79 insertions(+), 30 deletions(-) create mode 100644 app/components/ui/team-name-display.tsx diff --git a/app/components/standings/StandingsTable.tsx b/app/components/standings/StandingsTable.tsx index 82c4c17..923671b 100644 --- a/app/components/standings/StandingsTable.tsx +++ b/app/components/standings/StandingsTable.tsx @@ -1,6 +1,6 @@ -import { Link } from "react-router"; import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from "~/components/ui/table"; import { Badge } from "~/components/ui/badge"; +import { TeamNameDisplay } from "~/components/ui/team-name-display"; import { type TeamStanding } from "~/types/standings"; interface StandingsTableProps { @@ -55,12 +55,11 @@ export function StandingsTable({ - - {standing.teamName} - + {standing.actualPoints !== null && standing.actualPoints !== undefined diff --git a/app/components/ui/team-name-display.tsx b/app/components/ui/team-name-display.tsx new file mode 100644 index 0000000..b7b924a --- /dev/null +++ b/app/components/ui/team-name-display.tsx @@ -0,0 +1,31 @@ +import { Link } from "react-router"; + +interface TeamNameDisplayProps { + teamName: string; + ownerName?: string | null; + href?: string; +} + +/** + * Displays a team name with optional owner username below it. + * If href is provided, the team name is rendered as a link. + */ +export function TeamNameDisplay({ teamName, ownerName, href }: TeamNameDisplayProps) { + return ( +
+ {href ? ( + + {teamName} + + ) : ( +

{teamName}

+ )} + {ownerName && ( +

{ownerName}

+ )} +
+ ); +} diff --git a/app/models/standings.ts b/app/models/standings.ts index dce08b4..c98a1a2 100644 --- a/app/models/standings.ts +++ b/app/models/standings.ts @@ -1,11 +1,11 @@ import { database } from "~/database/context"; import * as schema from "~/database/schema"; import { eq, and, desc } from "drizzle-orm"; -import type { TeamStanding, TeamStandingSnapshot } from "~/types/standings"; +import type { TeamStanding, TeamStandingSnapshot, TeamStandingWithChange } from "~/types/standings"; import { calculateBracketPoints, calculateFantasyPoints } from "~/models/scoring-rules"; // Re-export types from shared types file -export type { TeamStanding, TeamStandingSnapshot } from "~/types/standings"; +export type { TeamStanding, TeamStandingSnapshot, TeamStandingWithChange } from "~/types/standings"; /** * Get current standings for a season @@ -291,7 +291,7 @@ export async function getTeamStandingsHistory( export async function getSevenDayStandingsChange( seasonId: string, providedDb?: ReturnType -) { +): Promise { const db = providedDb || database(); const sevenDaysAgo = new Date(); diff --git a/app/routes/leagues/$leagueId.standings.$seasonId.tsx b/app/routes/leagues/$leagueId.standings.$seasonId.tsx index 6851e75..4775dfa 100644 --- a/app/routes/leagues/$leagueId.standings.$seasonId.tsx +++ b/app/routes/leagues/$leagueId.standings.$seasonId.tsx @@ -9,6 +9,7 @@ import { StandingsTable } from "~/components/standings/StandingsTable"; import { getSevenDayStandingsChange, getSeasonPointProgression } from "~/models/standings"; import { isSeasonComplete, getSeasonCompletionPercentage } from "~/lib/season-helpers.server"; import { PointProgressionChart } from "~/components/standings/PointProgressionChart"; +import { findUsersByClerkIds, getUserDisplayName } from "~/models/user"; import type { Route } from "./+types/$leagueId.standings.$seasonId"; export function meta({ data }: Route.MetaArgs): Route.MetaDescriptors { @@ -70,22 +71,39 @@ export async function loader(args: Route.LoaderArgs) { }); } - // Fetch standings with 7-day comparison - const standingsWithComparison = await getSevenDayStandingsChange(seasonId, db); + // Fetch standings, teams (for owner lookup), and independent data in parallel + const [standingsWithComparison, teams, progressionData, seasonComplete, completionPercentage] = + await Promise.all([ + getSevenDayStandingsChange(seasonId, db), + db.query.teams.findMany({ + where: eq(schema.teams.seasonId, seasonId), + columns: { id: true, ownerId: true }, + }), + getSeasonPointProgression(seasonId, db), + isSeasonComplete(seasonId, db), + getSeasonCompletionPercentage(seasonId, db), + ]); + + // Build teamId -> ownerName map + const ownerIds = [...new Set(teams.map((t) => t.ownerId).filter((id): id is string => id != null))]; + const userRows = await findUsersByClerkIds(ownerIds); + const userByClerkId = new Map(userRows.map((u) => [u.clerkId, u])); + const ownerNameByTeamId = new Map( + teams + .filter((t): t is typeof t & { ownerId: string } => t.ownerId != null) + .map((t) => { + const user = userByClerkId.get(t.ownerId); + return [t.id, user ? getUserDisplayName(user) : null] as const; + }) + ); // Map to format expected by component (use sevenDayRankChange instead of previousRank) const formattedStandings = standingsWithComparison.map((standing) => ({ ...standing, rankChange: standing.sevenDayRankChange, + ownerName: ownerNameByTeamId.get(standing.teamId) ?? null, })); - // Fetch historical progression data - const progressionData = await getSeasonPointProgression(seasonId, db); - - // Check season completion status - const seasonComplete = await isSeasonComplete(seasonId, db); - const completionPercentage = await getSeasonCompletionPercentage(seasonId, db); - return { league, season, diff --git a/app/routes/leagues/$leagueId.tsx b/app/routes/leagues/$leagueId.tsx index f6c72c4..b85f1f5 100644 --- a/app/routes/leagues/$leagueId.tsx +++ b/app/routes/leagues/$leagueId.tsx @@ -14,6 +14,7 @@ import { } from "~/components/ui/card"; import { SportSeasonCard } from "~/components/sports/SportSeasonCard"; import { UpcomingCalendarPanel } from "~/components/sport-season/UpcomingCalendarPanel"; +import { TeamNameDisplay } from "~/components/ui/team-name-display"; import { getDisplayRank } from "~/lib/standings-display"; export function meta({ data }: Route.MetaArgs): Route.MetaDescriptors { @@ -196,17 +197,11 @@ export default function LeagueHome({ loaderData }: Route.ComponentProps) { {getDisplayRank(standing, standings.length)}
- - {team.name} - - {ownerName && ( -

- {ownerName} -

- )} +
{standing ? standing.totalPoints : 0} pts diff --git a/app/types/standings.ts b/app/types/standings.ts index 97697a5..643df61 100644 --- a/app/types/standings.ts +++ b/app/types/standings.ts @@ -5,6 +5,7 @@ export interface TeamStanding { teamId: string; teamName: string; + ownerName?: string | null; totalPoints: number; currentRank: number; previousRank: number | null; @@ -32,3 +33,8 @@ export interface TeamStandingSnapshot { rank: number; totalPoints: number; } + +export interface TeamStandingWithChange extends TeamStanding { + sevenDayRankChange: number; + sevenDayOldRank: number | null; +}