Simplify team breakdown page layout and fix code quality issues (#155)
- Replace per-sport cards with a single flat table; add Sport column (linked to sport season) before Participant - Replace three summary stat cards with a compact "X remaining" line in the header - Format pick numbers as round.pick notation (e.g. 2.02 for pick 16 in a 14-team league) - Add getNumTeamsInSeason model function to compute pick-within-round correctly - Remove dead bySport/totalPoints from standings model and component interface - Narrow standing prop type to only currentRank (placementCounts was unused) - Guard against numTeams === 0 edge case in pick notation - Fix 0.0 → 0.00 decimal inconsistency - Fix unsafe finalPosition! non-null assertion (null treated as Did Not Score) - Update tests to match current UI; add coverage for new edge cases Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
5a48327102
commit
33f9ad6ebe
5 changed files with 265 additions and 464 deletions
|
|
@ -1,12 +1,12 @@
|
||||||
import { Link } from "react-router";
|
import { Link } from "react-router";
|
||||||
import { Card, CardHeader, CardTitle, CardContent, CardDescription } from "~/components/ui/card";
|
import { Card, CardContent } from "~/components/ui/card";
|
||||||
import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from "~/components/ui/table";
|
import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from "~/components/ui/table";
|
||||||
import { Badge } from "~/components/ui/badge";
|
import { Badge } from "~/components/ui/badge";
|
||||||
import { Button } from "~/components/ui/button";
|
|
||||||
|
|
||||||
interface TeamScoreBreakdownProps {
|
interface TeamScoreBreakdownProps {
|
||||||
leagueId: string;
|
leagueId: string;
|
||||||
seasonId: string;
|
seasonId: string;
|
||||||
|
numTeams: number;
|
||||||
breakdown: {
|
breakdown: {
|
||||||
team: {
|
team: {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -26,21 +26,6 @@ interface TeamScoreBreakdownProps {
|
||||||
projectedPoints: number | null;
|
projectedPoints: number | null;
|
||||||
isComplete: boolean;
|
isComplete: boolean;
|
||||||
}>;
|
}>;
|
||||||
bySport: Record<string, { sportsSeasonId: string; picks: Array<{
|
|
||||||
pickNumber: number;
|
|
||||||
round: number;
|
|
||||||
participant: {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
sport: string;
|
|
||||||
sportsSeasonId: string;
|
|
||||||
};
|
|
||||||
finalPosition: number | null;
|
|
||||||
points: number;
|
|
||||||
projectedPoints: number | null;
|
|
||||||
isComplete: boolean;
|
|
||||||
}> }>;
|
|
||||||
totalPoints: number;
|
|
||||||
actualPoints: number;
|
actualPoints: number;
|
||||||
projectedPoints: number;
|
projectedPoints: number;
|
||||||
completedCount: number;
|
completedCount: number;
|
||||||
|
|
@ -48,16 +33,6 @@ interface TeamScoreBreakdownProps {
|
||||||
};
|
};
|
||||||
standing: {
|
standing: {
|
||||||
currentRank: number;
|
currentRank: number;
|
||||||
placementCounts: {
|
|
||||||
first: number;
|
|
||||||
second: number;
|
|
||||||
third: number;
|
|
||||||
fourth: number;
|
|
||||||
fifth: number;
|
|
||||||
sixth: number;
|
|
||||||
seventh: number;
|
|
||||||
eighth: number;
|
|
||||||
};
|
|
||||||
} | null;
|
} | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -68,6 +43,7 @@ interface TeamScoreBreakdownProps {
|
||||||
export function TeamScoreBreakdown({
|
export function TeamScoreBreakdown({
|
||||||
leagueId,
|
leagueId,
|
||||||
seasonId,
|
seasonId,
|
||||||
|
numTeams,
|
||||||
breakdown,
|
breakdown,
|
||||||
standing,
|
standing,
|
||||||
}: TeamScoreBreakdownProps) {
|
}: TeamScoreBreakdownProps) {
|
||||||
|
|
@ -79,31 +55,40 @@ export function TeamScoreBreakdown({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const sportEntries = Object.entries(breakdown.bySport).sort(([a], [b]) => a.localeCompare(b));
|
const remaining = breakdown.totalCount - breakdown.completedCount;
|
||||||
|
|
||||||
|
// Flatten all picks sorted by sport name then pick number
|
||||||
|
const allPicks = breakdown.picks
|
||||||
|
.slice()
|
||||||
|
.sort((a, b) => {
|
||||||
|
const sportCmp = a.participant.sport.localeCompare(b.participant.sport);
|
||||||
|
return sportCmp !== 0 ? sportCmp : a.pickNumber - b.pickNumber;
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Header with team info and summary */}
|
{/* Header */}
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold">{breakdown.team.name}</h1>
|
<h1 className="text-3xl font-bold">{breakdown.team.name}</h1>
|
||||||
<p className="text-muted-foreground mt-1">
|
<p className="text-muted-foreground mt-1">Team Score Breakdown</p>
|
||||||
Team Score Breakdown
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
<div className="text-4xl font-bold text-primary">
|
<div className="text-4xl font-bold text-primary">
|
||||||
{breakdown.actualPoints.toFixed(2)}
|
{breakdown.actualPoints.toFixed(2)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-muted-foreground">
|
<div className="text-sm text-muted-foreground">Actual Points</div>
|
||||||
Actual Points
|
|
||||||
</div>
|
|
||||||
{breakdown.projectedPoints > breakdown.actualPoints && (
|
{breakdown.projectedPoints > breakdown.actualPoints && (
|
||||||
<div className="text-xl font-semibold text-electric mt-1">
|
<div className="text-xl font-semibold text-electric mt-1">
|
||||||
{breakdown.projectedPoints.toFixed(2)}
|
{breakdown.projectedPoints.toFixed(2)}
|
||||||
<span className="text-xs text-muted-foreground ml-1">projected</span>
|
<span className="text-xs text-muted-foreground ml-1">projected</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{remaining > 0 && (
|
||||||
|
<div className="text-sm text-muted-foreground mt-1">
|
||||||
|
{remaining} participant{remaining !== 1 ? "s" : ""} remaining
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{standing && (
|
{standing && (
|
||||||
<Badge className="mt-2" variant={standing.currentRank <= 3 ? "default" : "outline"}>
|
<Badge className="mt-2" variant={standing.currentRank <= 3 ? "default" : "outline"}>
|
||||||
Rank #{standing.currentRank}
|
Rank #{standing.currentRank}
|
||||||
|
|
@ -112,214 +97,81 @@ export function TeamScoreBreakdown({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Summary stats */}
|
{/* All picks — single flat table */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
<Card>
|
||||||
<Card>
|
<CardContent className="pt-4">
|
||||||
<CardHeader className="pb-3">
|
<Table>
|
||||||
<CardTitle className="text-sm font-medium text-muted-foreground">
|
<TableHeader>
|
||||||
Actual Points
|
<TableRow>
|
||||||
</CardTitle>
|
<TableHead className="w-[90px]">Pick #</TableHead>
|
||||||
</CardHeader>
|
<TableHead>Sport</TableHead>
|
||||||
<CardContent>
|
<TableHead>Participant</TableHead>
|
||||||
<div className="text-2xl font-bold">
|
<TableHead className="text-center">Position</TableHead>
|
||||||
{breakdown.actualPoints.toFixed(2)}
|
<TableHead className="text-right">Points</TableHead>
|
||||||
</div>
|
</TableRow>
|
||||||
<p className="text-xs text-muted-foreground mt-1">
|
</TableHeader>
|
||||||
From {breakdown.completedCount} finished
|
<TableBody>
|
||||||
</p>
|
{allPicks.map((pick) => (
|
||||||
</CardContent>
|
<TableRow key={pick.pickNumber}>
|
||||||
</Card>
|
<TableCell className="text-muted-foreground">
|
||||||
|
{numTeams > 0
|
||||||
<Card>
|
? `${pick.round}.${String(pick.pickNumber - (pick.round - 1) * numTeams).padStart(2, "0")}`
|
||||||
<CardHeader className="pb-3">
|
: `#${pick.pickNumber}`}
|
||||||
<CardTitle className="text-sm font-medium text-muted-foreground">
|
<span className="text-xs ml-1">(#{pick.pickNumber})</span>
|
||||||
Projected Points
|
</TableCell>
|
||||||
</CardTitle>
|
<TableCell>
|
||||||
</CardHeader>
|
<Link
|
||||||
<CardContent>
|
to={`/leagues/${leagueId}/sports-seasons/${pick.participant.sportsSeasonId}`}
|
||||||
<div className="text-2xl font-bold text-primary">
|
className="text-sm font-medium hover:underline text-primary"
|
||||||
{breakdown.projectedPoints.toFixed(2)}
|
>
|
||||||
</div>
|
{pick.participant.sport}
|
||||||
<p className="text-xs text-muted-foreground mt-1">
|
</Link>
|
||||||
+{(breakdown.projectedPoints - breakdown.actualPoints).toFixed(2)} expected value
|
</TableCell>
|
||||||
</p>
|
<TableCell className="font-medium">
|
||||||
</CardContent>
|
{pick.participant.name}
|
||||||
</Card>
|
</TableCell>
|
||||||
|
<TableCell className="text-center">
|
||||||
<Card>
|
{pick.isComplete ? (
|
||||||
<CardHeader className="pb-3">
|
(pick.finalPosition ?? 0) === 0 ? (
|
||||||
<CardTitle className="text-sm font-medium text-muted-foreground">
|
<Badge variant="secondary">Did Not Score</Badge>
|
||||||
Participants
|
) : (
|
||||||
</CardTitle>
|
<PlacementBadge position={pick.finalPosition!} />
|
||||||
</CardHeader>
|
)
|
||||||
<CardContent>
|
) : (
|
||||||
<div className="text-2xl font-bold">
|
<Badge variant="outline">Pending</Badge>
|
||||||
{breakdown.completedCount} / {breakdown.totalCount}
|
)}
|
||||||
</div>
|
</TableCell>
|
||||||
<p className="text-xs text-muted-foreground mt-1">
|
<TableCell className="text-right">
|
||||||
{breakdown.totalCount - breakdown.completedCount} remaining
|
{pick.isComplete ? (
|
||||||
</p>
|
<span className="font-semibold">
|
||||||
</CardContent>
|
{pick.points > 0 ? pick.points.toFixed(2) : "0.00"}
|
||||||
</Card>
|
</span>
|
||||||
|
) : pick.projectedPoints !== null ? (
|
||||||
{standing && (
|
<div className="flex flex-col items-end">
|
||||||
<>
|
<span className="text-xs text-muted-foreground">Projected:</span>
|
||||||
<Card>
|
<span className="font-semibold text-primary">
|
||||||
<CardHeader className="pb-3">
|
{pick.projectedPoints.toFixed(2)}
|
||||||
<CardTitle className="text-sm font-medium text-muted-foreground">
|
</span>
|
||||||
Top Finishes
|
</div>
|
||||||
</CardTitle>
|
) : (
|
||||||
</CardHeader>
|
<span className="text-muted-foreground">-</span>
|
||||||
<CardContent>
|
)}
|
||||||
<div className="space-y-1">
|
</TableCell>
|
||||||
{standing.placementCounts.first > 0 && (
|
</TableRow>
|
||||||
<div className="flex justify-between text-sm">
|
))}
|
||||||
<span className="text-amber-accent font-medium">1st place</span>
|
</TableBody>
|
||||||
<span className="font-bold">{standing.placementCounts.first}</span>
|
</Table>
|
||||||
</div>
|
</CardContent>
|
||||||
)}
|
</Card>
|
||||||
{standing.placementCounts.second > 0 && (
|
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="text-muted-foreground font-medium">2nd place</span>
|
|
||||||
<span className="font-bold">{standing.placementCounts.second}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{standing.placementCounts.third > 0 && (
|
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="text-coral-accent font-medium">3rd place</span>
|
|
||||||
<span className="font-bold">{standing.placementCounts.third}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{standing.placementCounts.first === 0 &&
|
|
||||||
standing.placementCounts.second === 0 &&
|
|
||||||
standing.placementCounts.third === 0 && (
|
|
||||||
<p className="text-sm text-muted-foreground">No podium finishes yet</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="pb-3">
|
|
||||||
<CardTitle className="text-sm font-medium text-muted-foreground">
|
|
||||||
All Placements
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="grid grid-cols-2 gap-x-3 gap-y-1 text-xs">
|
|
||||||
{[
|
|
||||||
{ label: "4th", count: standing.placementCounts.fourth },
|
|
||||||
{ label: "5th", count: standing.placementCounts.fifth },
|
|
||||||
{ label: "6th", count: standing.placementCounts.sixth },
|
|
||||||
{ label: "7th", count: standing.placementCounts.seventh },
|
|
||||||
{ label: "8th", count: standing.placementCounts.eighth },
|
|
||||||
].map((item) => (
|
|
||||||
<div key={item.label} className="flex justify-between">
|
|
||||||
<span className="text-muted-foreground">{item.label}:</span>
|
|
||||||
<span className="font-medium">{item.count}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Participants grouped by sport */}
|
|
||||||
{sportEntries.map(([sportName, sportData]) => {
|
|
||||||
const { sportsSeasonId, picks: sportPicks } = sportData;
|
|
||||||
const sportTotal = sportPicks.reduce((sum: number, p) => sum + p.points, 0);
|
|
||||||
const sportCompleted = sportPicks.filter((p) => p.isComplete).length;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card key={sportName}>
|
|
||||||
<CardHeader>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<CardTitle>{sportName}</CardTitle>
|
|
||||||
<Button asChild variant="ghost" size="sm" className="h-6 px-2 text-xs">
|
|
||||||
<Link to={`/leagues/${leagueId}/sports-seasons/${sportsSeasonId}`}>
|
|
||||||
View Details →
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<CardDescription>
|
|
||||||
{sportPicks.length} {sportPicks.length === 1 ? 'pick' : 'picks'} · {sportCompleted} completed
|
|
||||||
</CardDescription>
|
|
||||||
</div>
|
|
||||||
<div className="text-right">
|
|
||||||
<div className="text-2xl font-bold">{sportTotal.toFixed(2)}</div>
|
|
||||||
<div className="text-xs text-muted-foreground">points</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<Table>
|
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead className="w-[100px]">Pick #</TableHead>
|
|
||||||
<TableHead>Participant</TableHead>
|
|
||||||
<TableHead className="text-center">Position</TableHead>
|
|
||||||
<TableHead className="text-right">Points</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{sportPicks.map((pick: typeof sportPicks[number]) => (
|
|
||||||
<TableRow key={pick.pickNumber}>
|
|
||||||
<TableCell className="text-muted-foreground">
|
|
||||||
#{pick.pickNumber}
|
|
||||||
<span className="text-xs ml-1">(R{pick.round})</span>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="font-medium">
|
|
||||||
{pick.participant.name}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-center">
|
|
||||||
{pick.isComplete ? (
|
|
||||||
pick.finalPosition === 0 ? (
|
|
||||||
<Badge variant="secondary">
|
|
||||||
Did Not Score
|
|
||||||
</Badge>
|
|
||||||
) : (
|
|
||||||
<PlacementBadge position={pick.finalPosition!} />
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<Badge variant="outline">Pending</Badge>
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-right">
|
|
||||||
{pick.isComplete ? (
|
|
||||||
<span className="font-semibold">
|
|
||||||
{pick.points > 0 ? pick.points.toFixed(2) : '0.0'}
|
|
||||||
</span>
|
|
||||||
) : pick.projectedPoints !== null ? (
|
|
||||||
<div className="flex flex-col items-end">
|
|
||||||
<span className="text-xs text-muted-foreground">Projected:</span>
|
|
||||||
<span className="font-semibold text-primary">
|
|
||||||
{pick.projectedPoints.toFixed(2)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<span className="text-muted-foreground">-</span>
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
{/* Navigation */}
|
{/* Navigation */}
|
||||||
<div className="flex justify-between pt-4">
|
<div className="flex justify-between pt-4">
|
||||||
<Button asChild variant="outline">
|
<Link
|
||||||
<Link to={`/leagues/${leagueId}/standings/${seasonId}`}>
|
to={`/leagues/${leagueId}/standings/${seasonId}`}
|
||||||
← Back to Standings
|
className="inline-flex items-center text-sm text-muted-foreground hover:text-foreground"
|
||||||
</Link>
|
>
|
||||||
</Button>
|
← Back to Standings
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -3,18 +3,15 @@ import { render, screen } from "@testing-library/react";
|
||||||
import { BrowserRouter } from "react-router";
|
import { BrowserRouter } from "react-router";
|
||||||
import { TeamScoreBreakdown } from "../TeamScoreBreakdown";
|
import { TeamScoreBreakdown } from "../TeamScoreBreakdown";
|
||||||
|
|
||||||
// Helper to wrap component with router context
|
|
||||||
function renderWithRouter(ui: React.ReactElement) {
|
function renderWithRouter(ui: React.ReactElement) {
|
||||||
return render(<BrowserRouter>{ui}</BrowserRouter>);
|
return render(<BrowserRouter>{ui}</BrowserRouter>);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* TeamScoreBreakdown Component Tests
|
|
||||||
* Phase 4.3: Team breakdown pages
|
|
||||||
*/
|
|
||||||
describe("TeamScoreBreakdown", () => {
|
describe("TeamScoreBreakdown", () => {
|
||||||
const mockLeagueId = "league-123";
|
const mockLeagueId = "league-123";
|
||||||
const mockSeasonId = "season-456";
|
const mockSeasonId = "season-456";
|
||||||
|
// 2-team league: pick 1 = 1.01, pick 2 = 1.02, pick 3 = 2.01
|
||||||
|
const numTeams = 2;
|
||||||
|
|
||||||
const mockBreakdown = {
|
const mockBreakdown = {
|
||||||
team: {
|
team: {
|
||||||
|
|
@ -65,87 +62,21 @@ describe("TeamScoreBreakdown", () => {
|
||||||
isComplete: false,
|
isComplete: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
bySport: {
|
|
||||||
NFL: {
|
|
||||||
sportsSeasonId: "ss-nfl",
|
|
||||||
picks: [
|
|
||||||
{
|
|
||||||
pickNumber: 1,
|
|
||||||
round: 1,
|
|
||||||
participant: {
|
|
||||||
id: "p1",
|
|
||||||
name: "Team A",
|
|
||||||
sport: "NFL",
|
|
||||||
sportsSeasonId: "ss-nfl",
|
|
||||||
},
|
|
||||||
finalPosition: 1,
|
|
||||||
points: 100,
|
|
||||||
projectedPoints: null,
|
|
||||||
isComplete: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
pickNumber: 3,
|
|
||||||
round: 2,
|
|
||||||
participant: {
|
|
||||||
id: "p3",
|
|
||||||
name: "Team C",
|
|
||||||
sport: "NFL",
|
|
||||||
sportsSeasonId: "ss-nfl",
|
|
||||||
},
|
|
||||||
finalPosition: null,
|
|
||||||
points: 0,
|
|
||||||
projectedPoints: 25,
|
|
||||||
isComplete: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
F1: {
|
|
||||||
sportsSeasonId: "ss-f1",
|
|
||||||
picks: [
|
|
||||||
{
|
|
||||||
pickNumber: 2,
|
|
||||||
round: 1,
|
|
||||||
participant: {
|
|
||||||
id: "p2",
|
|
||||||
name: "Driver B",
|
|
||||||
sport: "F1",
|
|
||||||
sportsSeasonId: "ss-f1",
|
|
||||||
},
|
|
||||||
finalPosition: 3,
|
|
||||||
points: 50,
|
|
||||||
projectedPoints: null,
|
|
||||||
isComplete: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
totalPoints: 150,
|
|
||||||
actualPoints: 150,
|
actualPoints: 150,
|
||||||
projectedPoints: 175,
|
projectedPoints: 175,
|
||||||
completedCount: 2,
|
completedCount: 2,
|
||||||
totalCount: 3,
|
totalCount: 3,
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockStanding = {
|
const mockStanding = { currentRank: 1 };
|
||||||
currentRank: 1,
|
|
||||||
placementCounts: {
|
|
||||||
first: 1,
|
|
||||||
second: 0,
|
|
||||||
third: 1,
|
|
||||||
fourth: 0,
|
|
||||||
fifth: 0,
|
|
||||||
sixth: 0,
|
|
||||||
seventh: 0,
|
|
||||||
eighth: 0,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
describe("Basic Rendering", () => {
|
describe("Basic Rendering", () => {
|
||||||
it("should render team name and total points", () => {
|
it("should render team name and actual points", () => {
|
||||||
renderWithRouter(
|
renderWithRouter(
|
||||||
<TeamScoreBreakdown
|
<TeamScoreBreakdown
|
||||||
leagueId={mockLeagueId}
|
leagueId={mockLeagueId}
|
||||||
seasonId={mockSeasonId}
|
seasonId={mockSeasonId}
|
||||||
|
numTeams={numTeams}
|
||||||
breakdown={mockBreakdown}
|
breakdown={mockBreakdown}
|
||||||
standing={mockStanding}
|
standing={mockStanding}
|
||||||
/>
|
/>
|
||||||
|
|
@ -155,11 +86,41 @@ describe("TeamScoreBreakdown", () => {
|
||||||
expect(screen.getAllByText("150.00").length).toBeGreaterThanOrEqual(1);
|
expect(screen.getAllByText("150.00").length).toBeGreaterThanOrEqual(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should show projected points when higher than actual", () => {
|
||||||
|
renderWithRouter(
|
||||||
|
<TeamScoreBreakdown
|
||||||
|
leagueId={mockLeagueId}
|
||||||
|
seasonId={mockSeasonId}
|
||||||
|
numTeams={numTeams}
|
||||||
|
breakdown={mockBreakdown}
|
||||||
|
standing={mockStanding}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText("175.00")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("projected")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show remaining participant count", () => {
|
||||||
|
renderWithRouter(
|
||||||
|
<TeamScoreBreakdown
|
||||||
|
leagueId={mockLeagueId}
|
||||||
|
seasonId={mockSeasonId}
|
||||||
|
numTeams={numTeams}
|
||||||
|
breakdown={mockBreakdown}
|
||||||
|
standing={mockStanding}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText("1 participant remaining")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
it("should show team rank badge", () => {
|
it("should show team rank badge", () => {
|
||||||
renderWithRouter(
|
renderWithRouter(
|
||||||
<TeamScoreBreakdown
|
<TeamScoreBreakdown
|
||||||
leagueId={mockLeagueId}
|
leagueId={mockLeagueId}
|
||||||
seasonId={mockSeasonId}
|
seasonId={mockSeasonId}
|
||||||
|
numTeams={numTeams}
|
||||||
breakdown={mockBreakdown}
|
breakdown={mockBreakdown}
|
||||||
standing={mockStanding}
|
standing={mockStanding}
|
||||||
/>
|
/>
|
||||||
|
|
@ -168,28 +129,13 @@ describe("TeamScoreBreakdown", () => {
|
||||||
expect(screen.getByText("Rank #1")).toBeInTheDocument();
|
expect(screen.getByText("Rank #1")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should display participant completion stats", () => {
|
|
||||||
renderWithRouter(
|
|
||||||
<TeamScoreBreakdown
|
|
||||||
leagueId={mockLeagueId}
|
|
||||||
seasonId={mockSeasonId}
|
|
||||||
breakdown={mockBreakdown}
|
|
||||||
standing={mockStanding}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(screen.getByText("2 / 3")).toBeInTheDocument();
|
|
||||||
expect(screen.getByText("1 remaining")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should handle null team", () => {
|
it("should handle null team", () => {
|
||||||
const nullBreakdown = { ...mockBreakdown, team: null };
|
|
||||||
|
|
||||||
renderWithRouter(
|
renderWithRouter(
|
||||||
<TeamScoreBreakdown
|
<TeamScoreBreakdown
|
||||||
leagueId={mockLeagueId}
|
leagueId={mockLeagueId}
|
||||||
seasonId={mockSeasonId}
|
seasonId={mockSeasonId}
|
||||||
breakdown={nullBreakdown}
|
numTeams={numTeams}
|
||||||
|
breakdown={{ ...mockBreakdown, team: null }}
|
||||||
standing={mockStanding}
|
standing={mockStanding}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
@ -198,66 +144,63 @@ describe("TeamScoreBreakdown", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Sport Grouping", () => {
|
describe("Pick Number Display", () => {
|
||||||
it("should group participants by sport", () => {
|
it("should format picks as round.pick notation", () => {
|
||||||
renderWithRouter(
|
renderWithRouter(
|
||||||
<TeamScoreBreakdown
|
<TeamScoreBreakdown
|
||||||
leagueId={mockLeagueId}
|
leagueId={mockLeagueId}
|
||||||
seasonId={mockSeasonId}
|
seasonId={mockSeasonId}
|
||||||
|
numTeams={numTeams}
|
||||||
breakdown={mockBreakdown}
|
breakdown={mockBreakdown}
|
||||||
standing={mockStanding}
|
standing={mockStanding}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.getByText("NFL")).toBeInTheDocument();
|
expect(screen.getByText("1.01")).toBeInTheDocument();
|
||||||
expect(screen.getByText("F1")).toBeInTheDocument();
|
expect(screen.getByText("1.02")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("2.01")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should show correct pick counts per sport", () => {
|
it("should show overall pick number in parentheses", () => {
|
||||||
renderWithRouter(
|
renderWithRouter(
|
||||||
<TeamScoreBreakdown
|
<TeamScoreBreakdown
|
||||||
leagueId={mockLeagueId}
|
leagueId={mockLeagueId}
|
||||||
seasonId={mockSeasonId}
|
seasonId={mockSeasonId}
|
||||||
|
numTeams={numTeams}
|
||||||
breakdown={mockBreakdown}
|
breakdown={mockBreakdown}
|
||||||
standing={mockStanding}
|
standing={mockStanding}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
// NFL has 2 picks
|
expect(screen.getByText("(#1)")).toBeInTheDocument();
|
||||||
const nflSection = screen.getByText("2 picks · 1 completed");
|
expect(screen.getByText("(#2)")).toBeInTheDocument();
|
||||||
expect(nflSection).toBeInTheDocument();
|
expect(screen.getByText("(#3)")).toBeInTheDocument();
|
||||||
|
|
||||||
// F1 has 1 pick
|
|
||||||
const f1Section = screen.getByText("1 pick · 1 completed");
|
|
||||||
expect(f1Section).toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should calculate sport-specific point totals", () => {
|
it("should fall back to #pickNumber when numTeams is 0", () => {
|
||||||
renderWithRouter(
|
renderWithRouter(
|
||||||
<TeamScoreBreakdown
|
<TeamScoreBreakdown
|
||||||
leagueId={mockLeagueId}
|
leagueId={mockLeagueId}
|
||||||
seasonId={mockSeasonId}
|
seasonId={mockSeasonId}
|
||||||
|
numTeams={0}
|
||||||
breakdown={mockBreakdown}
|
breakdown={mockBreakdown}
|
||||||
standing={mockStanding}
|
standing={mockStanding}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
// NFL total should be 100 (Team A)
|
expect(screen.getByText("#1")).toBeInTheDocument();
|
||||||
// F1 total should be 50 (Driver B)
|
expect(screen.getByText("#2")).toBeInTheDocument();
|
||||||
const pointCells = screen.getAllByText(/^\d+\.\d\d$/)
|
expect(screen.getByText("#3")).toBeInTheDocument();
|
||||||
.map((el) => parseFloat(el.textContent!));
|
|
||||||
|
|
||||||
expect(pointCells).toContain(100.0);
|
|
||||||
expect(pointCells).toContain(50.0);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Participant Display", () => {
|
describe("Participant Display", () => {
|
||||||
it("should display all participant names", () => {
|
it("should display all participant names in a single table", () => {
|
||||||
renderWithRouter(
|
renderWithRouter(
|
||||||
<TeamScoreBreakdown
|
<TeamScoreBreakdown
|
||||||
leagueId={mockLeagueId}
|
leagueId={mockLeagueId}
|
||||||
seasonId={mockSeasonId}
|
seasonId={mockSeasonId}
|
||||||
|
numTeams={numTeams}
|
||||||
breakdown={mockBreakdown}
|
breakdown={mockBreakdown}
|
||||||
standing={mockStanding}
|
standing={mockStanding}
|
||||||
/>
|
/>
|
||||||
|
|
@ -268,22 +211,29 @@ describe("TeamScoreBreakdown", () => {
|
||||||
expect(screen.getByText("Team C")).toBeInTheDocument();
|
expect(screen.getByText("Team C")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should show pick numbers and rounds", () => {
|
it("should show sport names as links to sport season pages", () => {
|
||||||
renderWithRouter(
|
renderWithRouter(
|
||||||
<TeamScoreBreakdown
|
<TeamScoreBreakdown
|
||||||
leagueId={mockLeagueId}
|
leagueId={mockLeagueId}
|
||||||
seasonId={mockSeasonId}
|
seasonId={mockSeasonId}
|
||||||
|
numTeams={numTeams}
|
||||||
breakdown={mockBreakdown}
|
breakdown={mockBreakdown}
|
||||||
standing={mockStanding}
|
standing={mockStanding}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.getByText("#1")).toBeInTheDocument();
|
const nflLinks = screen.getAllByRole("link", { name: "NFL" });
|
||||||
expect(screen.getByText("#2")).toBeInTheDocument();
|
expect(nflLinks.length).toBeGreaterThanOrEqual(1);
|
||||||
expect(screen.getByText("#3")).toBeInTheDocument();
|
expect(nflLinks[0]).toHaveAttribute(
|
||||||
|
"href",
|
||||||
|
`/leagues/${mockLeagueId}/sports-seasons/ss-nfl`
|
||||||
|
);
|
||||||
|
|
||||||
expect(screen.getAllByText("(R1)")).toHaveLength(2);
|
const f1Link = screen.getByRole("link", { name: "F1" });
|
||||||
expect(screen.getByText("(R2)")).toBeInTheDocument();
|
expect(f1Link).toHaveAttribute(
|
||||||
|
"href",
|
||||||
|
`/leagues/${mockLeagueId}/sports-seasons/ss-f1`
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should show placement badges for completed participants", () => {
|
it("should show placement badges for completed participants", () => {
|
||||||
|
|
@ -291,6 +241,7 @@ describe("TeamScoreBreakdown", () => {
|
||||||
<TeamScoreBreakdown
|
<TeamScoreBreakdown
|
||||||
leagueId={mockLeagueId}
|
leagueId={mockLeagueId}
|
||||||
seasonId={mockSeasonId}
|
seasonId={mockSeasonId}
|
||||||
|
numTeams={numTeams}
|
||||||
breakdown={mockBreakdown}
|
breakdown={mockBreakdown}
|
||||||
standing={mockStanding}
|
standing={mockStanding}
|
||||||
/>
|
/>
|
||||||
|
|
@ -300,11 +251,12 @@ describe("TeamScoreBreakdown", () => {
|
||||||
expect(screen.getByText("3rd")).toBeInTheDocument();
|
expect(screen.getByText("3rd")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should show pending badge for incomplete participants", () => {
|
it("should show Pending badge for incomplete participants", () => {
|
||||||
renderWithRouter(
|
renderWithRouter(
|
||||||
<TeamScoreBreakdown
|
<TeamScoreBreakdown
|
||||||
leagueId={mockLeagueId}
|
leagueId={mockLeagueId}
|
||||||
seasonId={mockSeasonId}
|
seasonId={mockSeasonId}
|
||||||
|
numTeams={numTeams}
|
||||||
breakdown={mockBreakdown}
|
breakdown={mockBreakdown}
|
||||||
standing={mockStanding}
|
standing={mockStanding}
|
||||||
/>
|
/>
|
||||||
|
|
@ -313,109 +265,109 @@ describe("TeamScoreBreakdown", () => {
|
||||||
expect(screen.getByText("Pending")).toBeInTheDocument();
|
expect(screen.getByText("Pending")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should display points for each participant", () => {
|
it("should show Did Not Score badge when finalPosition is 0", () => {
|
||||||
renderWithRouter(
|
const breakdownWithDNS = {
|
||||||
<TeamScoreBreakdown
|
...mockBreakdown,
|
||||||
leagueId={mockLeagueId}
|
picks: [
|
||||||
seasonId={mockSeasonId}
|
{
|
||||||
breakdown={mockBreakdown}
|
...mockBreakdown.picks[0],
|
||||||
standing={mockStanding}
|
finalPosition: 0,
|
||||||
/>
|
points: 0,
|
||||||
);
|
},
|
||||||
|
],
|
||||||
// Should have 100.00 and 50.00 for completed picks, and "-" for pending
|
|
||||||
const pointValues = screen.getAllByRole("cell")
|
|
||||||
.filter((cell) => cell.textContent?.match(/^\d+\.\d\d$|^-$/));
|
|
||||||
|
|
||||||
expect(pointValues.length).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Placement Summary", () => {
|
|
||||||
it("should show top finishes in summary card", () => {
|
|
||||||
renderWithRouter(
|
|
||||||
<TeamScoreBreakdown
|
|
||||||
leagueId={mockLeagueId}
|
|
||||||
seasonId={mockSeasonId}
|
|
||||||
breakdown={mockBreakdown}
|
|
||||||
standing={mockStanding}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(screen.getByText("1st place")).toBeInTheDocument();
|
|
||||||
expect(screen.getByText("3rd place")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not show placements with zero count", () => {
|
|
||||||
renderWithRouter(
|
|
||||||
<TeamScoreBreakdown
|
|
||||||
leagueId={mockLeagueId}
|
|
||||||
seasonId={mockSeasonId}
|
|
||||||
breakdown={mockBreakdown}
|
|
||||||
standing={mockStanding}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(screen.queryByText("2nd place")).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should show message when no podium finishes", () => {
|
|
||||||
const noPodiumStanding = {
|
|
||||||
currentRank: 5,
|
|
||||||
placementCounts: {
|
|
||||||
first: 0,
|
|
||||||
second: 0,
|
|
||||||
third: 0,
|
|
||||||
fourth: 1,
|
|
||||||
fifth: 1,
|
|
||||||
sixth: 0,
|
|
||||||
seventh: 0,
|
|
||||||
eighth: 0,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
renderWithRouter(
|
renderWithRouter(
|
||||||
<TeamScoreBreakdown
|
<TeamScoreBreakdown
|
||||||
leagueId={mockLeagueId}
|
leagueId={mockLeagueId}
|
||||||
seasonId={mockSeasonId}
|
seasonId={mockSeasonId}
|
||||||
breakdown={mockBreakdown}
|
numTeams={numTeams}
|
||||||
standing={noPodiumStanding}
|
breakdown={breakdownWithDNS}
|
||||||
|
standing={mockStanding}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.getByText("No podium finishes yet")).toBeInTheDocument();
|
expect(screen.getByText("Did Not Score")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show Did Not Score badge when isComplete but finalPosition is null", () => {
|
||||||
|
const breakdownWithNullPosition = {
|
||||||
|
...mockBreakdown,
|
||||||
|
picks: [
|
||||||
|
{
|
||||||
|
...mockBreakdown.picks[0],
|
||||||
|
finalPosition: null,
|
||||||
|
points: 0,
|
||||||
|
isComplete: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
renderWithRouter(
|
||||||
|
<TeamScoreBreakdown
|
||||||
|
leagueId={mockLeagueId}
|
||||||
|
seasonId={mockSeasonId}
|
||||||
|
numTeams={numTeams}
|
||||||
|
breakdown={breakdownWithNullPosition}
|
||||||
|
standing={mockStanding}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText("Did Not Score")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show projected points for incomplete participants", () => {
|
||||||
|
renderWithRouter(
|
||||||
|
<TeamScoreBreakdown
|
||||||
|
leagueId={mockLeagueId}
|
||||||
|
seasonId={mockSeasonId}
|
||||||
|
numTeams={numTeams}
|
||||||
|
breakdown={mockBreakdown}
|
||||||
|
standing={mockStanding}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText("Projected:")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("25.00")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should display 0.00 for completed picks with zero points", () => {
|
||||||
|
const breakdownWithZero = {
|
||||||
|
...mockBreakdown,
|
||||||
|
picks: [{ ...mockBreakdown.picks[0], finalPosition: 0, points: 0 }],
|
||||||
|
};
|
||||||
|
|
||||||
|
renderWithRouter(
|
||||||
|
<TeamScoreBreakdown
|
||||||
|
leagueId={mockLeagueId}
|
||||||
|
seasonId={mockSeasonId}
|
||||||
|
numTeams={numTeams}
|
||||||
|
breakdown={breakdownWithZero}
|
||||||
|
standing={mockStanding}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText("0.00")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Navigation Links", () => {
|
describe("Navigation", () => {
|
||||||
it("should have back to standings link", () => {
|
it("should have back to standings link", () => {
|
||||||
renderWithRouter(
|
renderWithRouter(
|
||||||
<TeamScoreBreakdown
|
<TeamScoreBreakdown
|
||||||
leagueId={mockLeagueId}
|
leagueId={mockLeagueId}
|
||||||
seasonId={mockSeasonId}
|
seasonId={mockSeasonId}
|
||||||
|
numTeams={numTeams}
|
||||||
breakdown={mockBreakdown}
|
breakdown={mockBreakdown}
|
||||||
standing={mockStanding}
|
standing={mockStanding}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
const backLink = screen.getByRole("link", { name: /back to standings/i });
|
const backLink = screen.getByRole("link", { name: /back to standings/i });
|
||||||
expect(backLink).toHaveAttribute("href", `/leagues/${mockLeagueId}/standings/${mockSeasonId}`);
|
expect(backLink).toHaveAttribute(
|
||||||
});
|
"href",
|
||||||
|
`/leagues/${mockLeagueId}/standings/${mockSeasonId}`
|
||||||
it("should have links to sport season pages", () => {
|
|
||||||
renderWithRouter(
|
|
||||||
<TeamScoreBreakdown
|
|
||||||
leagueId={mockLeagueId}
|
|
||||||
seasonId={mockSeasonId}
|
|
||||||
breakdown={mockBreakdown}
|
|
||||||
standing={mockStanding}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const viewDetailsLinks = screen.getAllByRole("link", { name: /view details/i });
|
|
||||||
expect(viewDetailsLinks).toHaveLength(2); // One for NFL, one for F1
|
|
||||||
|
|
||||||
expect(viewDetailsLinks[0]).toHaveAttribute("href", expect.stringContaining("/sports-seasons/"));
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -425,16 +377,13 @@ describe("TeamScoreBreakdown", () => {
|
||||||
<TeamScoreBreakdown
|
<TeamScoreBreakdown
|
||||||
leagueId={mockLeagueId}
|
leagueId={mockLeagueId}
|
||||||
seasonId={mockSeasonId}
|
seasonId={mockSeasonId}
|
||||||
|
numTeams={numTeams}
|
||||||
breakdown={mockBreakdown}
|
breakdown={mockBreakdown}
|
||||||
standing={null}
|
standing={null}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
// Should still show team name and points
|
|
||||||
expect(screen.getByText("Test Team")).toBeInTheDocument();
|
expect(screen.getByText("Test Team")).toBeInTheDocument();
|
||||||
expect(screen.getAllByText("150.00").length).toBeGreaterThanOrEqual(1);
|
|
||||||
|
|
||||||
// But not show rank badge
|
|
||||||
expect(screen.queryByText(/Rank #/)).not.toBeInTheDocument();
|
expect(screen.queryByText(/Rank #/)).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { eq } from "drizzle-orm";
|
import { eq, count } from "drizzle-orm";
|
||||||
import { database } from "~/database/context";
|
import { database } from "~/database/context";
|
||||||
import * as schema from "~/database/schema";
|
import * as schema from "~/database/schema";
|
||||||
|
|
||||||
|
|
@ -126,6 +126,18 @@ export async function setDraftOrder(
|
||||||
return await createManyDraftSlots(slots);
|
return await createManyDraftSlots(slots);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the number of teams (draft slots) in a season
|
||||||
|
*/
|
||||||
|
export async function getNumTeamsInSeason(seasonId: string): Promise<number> {
|
||||||
|
const db = database();
|
||||||
|
const [result] = await db
|
||||||
|
.select({ count: count() })
|
||||||
|
.from(schema.draftSlots)
|
||||||
|
.where(eq(schema.draftSlots.seasonId, seasonId));
|
||||||
|
return result?.count ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Randomize the draft order for a season
|
* Randomize the draft order for a season
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -218,20 +218,6 @@ export async function getTeamScoreBreakdown(
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// Group by sport for easier display
|
|
||||||
// Structure: { sportName: { sportsSeasonId, picks } }
|
|
||||||
const bySport: Record<string, { sportsSeasonId: string; picks: typeof pickBreakdown }> = {};
|
|
||||||
for (const pick of pickBreakdown) {
|
|
||||||
const sport = pick.participant.sport;
|
|
||||||
if (!bySport[sport]) {
|
|
||||||
bySport[sport] = {
|
|
||||||
sportsSeasonId: pick.participant.sportsSeasonId,
|
|
||||||
picks: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
bySport[sport].picks.push(pick);
|
|
||||||
}
|
|
||||||
|
|
||||||
const actualPoints = pickBreakdown
|
const actualPoints = pickBreakdown
|
||||||
.filter((p) => p.isComplete)
|
.filter((p) => p.isComplete)
|
||||||
.reduce((sum, p) => sum + p.points, 0);
|
.reduce((sum, p) => sum + p.points, 0);
|
||||||
|
|
@ -246,8 +232,6 @@ export async function getTeamScoreBreakdown(
|
||||||
where: eq(schema.teams.id, teamId),
|
where: eq(schema.teams.id, teamId),
|
||||||
}),
|
}),
|
||||||
picks: pickBreakdown,
|
picks: pickBreakdown,
|
||||||
bySport,
|
|
||||||
totalPoints: pickBreakdown.reduce((sum, p) => sum + p.points, 0),
|
|
||||||
actualPoints,
|
actualPoints,
|
||||||
projectedPoints: projectedTotalPoints,
|
projectedPoints: projectedTotalPoints,
|
||||||
completedCount: pickBreakdown.filter((p) => p.isComplete).length,
|
completedCount: pickBreakdown.filter((p) => p.isComplete).length,
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import type { Route } from "./+types/$leagueId.standings.$seasonId.teams.$teamId
|
||||||
|
|
||||||
import { TeamScoreBreakdown } from "~/components/standings/TeamScoreBreakdown";
|
import { TeamScoreBreakdown } from "~/components/standings/TeamScoreBreakdown";
|
||||||
import { getTeamScoreBreakdown, getTeamStanding } from "~/models/standings";
|
import { getTeamScoreBreakdown, getTeamStanding } from "~/models/standings";
|
||||||
|
import { getNumTeamsInSeason } from "~/models/draft-slot";
|
||||||
|
|
||||||
export function meta({ data }: Route.MetaArgs): Route.MetaDescriptors {
|
export function meta({ data }: Route.MetaArgs): Route.MetaDescriptors {
|
||||||
return [{ title: `${data?.breakdown?.team?.name ?? "Team"} - Brackt` }];
|
return [{ title: `${data?.breakdown?.team?.name ?? "Team"} - Brackt` }];
|
||||||
|
|
@ -17,15 +18,16 @@ export async function loader({ params }: Route.LoaderArgs) {
|
||||||
const { leagueId, seasonId, teamId } = params;
|
const { leagueId, seasonId, teamId } = params;
|
||||||
|
|
||||||
// Get team breakdown with all picks
|
// Get team breakdown with all picks
|
||||||
const breakdown = await getTeamScoreBreakdown(teamId, seasonId);
|
const [breakdown, standing, numTeams] = await Promise.all([
|
||||||
|
getTeamScoreBreakdown(teamId, seasonId),
|
||||||
|
getTeamStanding(teamId, seasonId),
|
||||||
|
getNumTeamsInSeason(seasonId),
|
||||||
|
]);
|
||||||
|
|
||||||
if (!breakdown) {
|
if (!breakdown) {
|
||||||
throw new Response("Team not found", { status: 404 });
|
throw new Response("Team not found", { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get team standing for additional context
|
|
||||||
const standing = await getTeamStanding(teamId, seasonId);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
leagueId,
|
leagueId,
|
||||||
seasonId,
|
seasonId,
|
||||||
|
|
@ -35,11 +37,12 @@ export async function loader({ params }: Route.LoaderArgs) {
|
||||||
team: breakdown.team || null,
|
team: breakdown.team || null,
|
||||||
},
|
},
|
||||||
standing,
|
standing,
|
||||||
|
numTeams,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TeamBreakdownPage() {
|
export default function TeamBreakdownPage() {
|
||||||
const { leagueId, seasonId, breakdown, standing } = useLoaderData<typeof loader>();
|
const { leagueId, seasonId, breakdown, standing, numTeams } = useLoaderData<typeof loader>();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto py-8 px-4">
|
<div className="container mx-auto py-8 px-4">
|
||||||
|
|
@ -48,6 +51,7 @@ export default function TeamBreakdownPage() {
|
||||||
seasonId={seasonId}
|
seasonId={seasonId}
|
||||||
breakdown={breakdown}
|
breakdown={breakdown}
|
||||||
standing={standing}
|
standing={standing}
|
||||||
|
numTeams={numTeams}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue