Add projections to full standings; clean up change indicators
All checks were successful
🚀 Deploy / 🧪 Test (pull_request) Successful in 3m23s
🚀 Deploy / 🧪 Test (push) Successful in 3m9s
🚀 Deploy / ʦ🔍 Typecheck & Lint (push) Successful in 1m20s
🚀 Deploy / 🐳 Build (push) Successful in 1m9s
🚀 Deploy / ʦ🔍 Typecheck & Lint (pull_request) Successful in 1m23s
🚀 Deploy / 🐳 Build (pull_request) Has been skipped
🚀 Deploy / 🚀 Deploy (pull_request) Has been skipped
🚀 Deploy / 🚀 Deploy (push) Successful in 12s
All checks were successful
🚀 Deploy / 🧪 Test (pull_request) Successful in 3m23s
🚀 Deploy / 🧪 Test (push) Successful in 3m9s
🚀 Deploy / ʦ🔍 Typecheck & Lint (push) Successful in 1m20s
🚀 Deploy / 🐳 Build (push) Successful in 1m9s
🚀 Deploy / ʦ🔍 Typecheck & Lint (pull_request) Successful in 1m23s
🚀 Deploy / 🐳 Build (pull_request) Has been skipped
🚀 Deploy / 🚀 Deploy (pull_request) Has been skipped
🚀 Deploy / 🚀 Deploy (push) Successful in 12s
Surface projected final points on the full standings page and restructure the rank/point change indicators so columns align cleanly on desktop and mobile. - StatHelpers: stacked label/value/delta columns with a per-row reservable delta line; merge rank/point indicators into a single DeltaBadge; parameterize StatDivider height. - StandingsPreview: opt-in `showProjected` column (projected points only), gated on participants remaining; reserve the delta line only when a row actually moved (no stray em-dashes). - Full standings page: pass projected data + showProjected (hidden when the season is complete). - League home: fetch via getSevenDayStandingsChange so the preview shows the same 7-day rank/point changes as the full standings page. - LeagueRow: top-align stats and restore h-8 dividers so the shared-component changes don't alter the league list rows. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
4a5ea6fdc5
commit
b960d39be3
8 changed files with 266 additions and 70 deletions
|
|
@ -118,20 +118,23 @@ function ActiveRow({
|
||||||
</div>
|
</div>
|
||||||
{/* Stats — second row on mobile, right side on desktop */}
|
{/* Stats — second row on mobile, right side on desktop */}
|
||||||
{showStats && (
|
{showStats && (
|
||||||
<div className="flex items-center gap-4 border-t border-border/50 pt-2 sm:border-0 sm:pt-0 sm:shrink-0">
|
<div className="flex items-start gap-4 border-t border-border/50 pt-2 sm:border-0 sm:pt-0 sm:shrink-0">
|
||||||
{displayRank !== undefined && (
|
{displayRank !== undefined && (
|
||||||
<RankingDisplay
|
<RankingDisplay
|
||||||
displayRank={displayRank}
|
displayRank={displayRank}
|
||||||
rankChange={previousRank !== undefined && currentRank !== undefined && previousRank !== currentRank ? previousRank - currentRank : undefined}
|
rankChange={previousRank !== undefined && currentRank !== undefined && previousRank !== currentRank ? previousRank - currentRank : undefined}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{currentRank !== undefined && totalPoints !== undefined && <StatDivider />}
|
{currentRank !== undefined && totalPoints !== undefined && <StatDivider className="h-8" />}
|
||||||
{totalPoints !== undefined && (
|
{totalPoints !== undefined && (
|
||||||
<StatColumn label="Points">
|
<StatColumn
|
||||||
<span className="text-2xl font-bold leading-none text-electric">
|
label="Points"
|
||||||
{Math.round(totalPoints).toLocaleString("en-US")}
|
value={
|
||||||
</span>
|
<span className="text-2xl font-bold leading-none text-electric">
|
||||||
</StatColumn>
|
{Math.round(totalPoints).toLocaleString("en-US")}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -169,17 +172,21 @@ function PreDraftRow({
|
||||||
</div>
|
</div>
|
||||||
{/* Stats — second row on mobile, right side on desktop */}
|
{/* Stats — second row on mobile, right side on desktop */}
|
||||||
<div className="flex items-center gap-4 border-t border-border/50 pt-2 sm:border-0 sm:pt-0 sm:shrink-0">
|
<div className="flex items-center gap-4 border-t border-border/50 pt-2 sm:border-0 sm:pt-0 sm:shrink-0">
|
||||||
<StatColumn label="Draft">
|
<StatColumn
|
||||||
<span className="text-2xl font-bold leading-none">{draftTimeValue}</span>
|
label="Draft"
|
||||||
</StatColumn>
|
value={<span className="text-2xl font-bold leading-none">{draftTimeValue}</span>}
|
||||||
|
/>
|
||||||
{draftPosition !== undefined && (
|
{draftPosition !== undefined && (
|
||||||
<>
|
<>
|
||||||
<StatDivider />
|
<StatDivider className="h-8" />
|
||||||
<StatColumn label="Position">
|
<StatColumn
|
||||||
<span className="text-2xl font-bold leading-none">
|
label="Position"
|
||||||
{ordinal(draftPosition)}
|
value={
|
||||||
</span>
|
<span className="text-2xl font-bold leading-none">
|
||||||
</StatColumn>
|
{ordinal(draftPosition)}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -137,6 +137,52 @@ export const FullLeague: Story = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const WithProjections: Story = {
|
||||||
|
args: {
|
||||||
|
showProjected: true,
|
||||||
|
entries: [
|
||||||
|
{
|
||||||
|
teamId: "t1",
|
||||||
|
teamName: "Lightning Wolves",
|
||||||
|
ownerName: "alice",
|
||||||
|
displayRank: 1,
|
||||||
|
currentRank: 1,
|
||||||
|
points: 2810,
|
||||||
|
rankChange: 2,
|
||||||
|
pointChange: 87.5,
|
||||||
|
projectedPoints: 3120,
|
||||||
|
participantsRemaining: 3,
|
||||||
|
href: "/leagues/1/standings/1/teams/t1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
teamId: "t2",
|
||||||
|
teamName: "Shadow Hawks",
|
||||||
|
ownerName: "bob",
|
||||||
|
displayRank: 2,
|
||||||
|
currentRank: 2,
|
||||||
|
points: 2654.5,
|
||||||
|
rankChange: -1,
|
||||||
|
pointChange: 42.0,
|
||||||
|
projectedPoints: 2980,
|
||||||
|
participantsRemaining: 2,
|
||||||
|
href: "/leagues/1/standings/1/teams/t2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
teamId: "t3",
|
||||||
|
teamName: "Iron Eagles",
|
||||||
|
ownerName: "carol",
|
||||||
|
displayRank: 3,
|
||||||
|
currentRank: 3,
|
||||||
|
points: 2493,
|
||||||
|
// No 7-day change and no live participants — placeholders keep alignment.
|
||||||
|
projectedPoints: 2493,
|
||||||
|
participantsRemaining: 0,
|
||||||
|
href: "/leagues/1/standings/1/teams/t3",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export const WithTies: Story = {
|
export const WithTies: Story = {
|
||||||
args: {
|
args: {
|
||||||
entries: [
|
entries: [
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,17 @@ import { Card, CardContent, CardHeader } from "~/components/ui/card";
|
||||||
import { GradientIcon } from "~/components/ui/GradientIcon";
|
import { GradientIcon } from "~/components/ui/GradientIcon";
|
||||||
import { TeamAvatar } from "~/components/TeamAvatar";
|
import { TeamAvatar } from "~/components/TeamAvatar";
|
||||||
import type { AvatarData, RawFlagConfig } from "~/lib/flag-types";
|
import type { AvatarData, RawFlagConfig } from "~/lib/flag-types";
|
||||||
import { StatColumn, StatDivider, PointChangeIndicator, RankingDisplay } from "./StatHelpers";
|
import { StatColumn, StatDivider, DeltaBadge, RankingDisplay } from "./StatHelpers";
|
||||||
|
|
||||||
|
/** Mirrors PointsDisplay gating: only project while the team still has participants live. */
|
||||||
|
function hasProjection(entry: StandingsPreviewEntry): boolean {
|
||||||
|
return (
|
||||||
|
entry.projectedPoints !== null &&
|
||||||
|
entry.projectedPoints !== undefined &&
|
||||||
|
entry.participantsRemaining !== undefined &&
|
||||||
|
entry.participantsRemaining > 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Row styles ───────────────────────────────────────────────────────────────
|
// ─── Row styles ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
@ -23,7 +33,20 @@ function rowClasses(currentRank: number | undefined, hasHref: boolean): string {
|
||||||
|
|
||||||
// ─── Row content ──────────────────────────────────────────────────────────────
|
// ─── Row content ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function RowContent({ entry }: { entry: StandingsPreviewEntry }) {
|
function RowContent({
|
||||||
|
entry,
|
||||||
|
showProjected,
|
||||||
|
}: {
|
||||||
|
entry: StandingsPreviewEntry;
|
||||||
|
showProjected: boolean;
|
||||||
|
}) {
|
||||||
|
// Only reserve the delta line when this row actually changed. A row with no rank
|
||||||
|
// or point movement drops the line entirely (no "—" placeholders); rows that moved
|
||||||
|
// keep aligned columns by showing "—" for whichever stat didn't change.
|
||||||
|
const rowHasChange =
|
||||||
|
(entry.rankChange !== undefined && entry.rankChange !== 0) ||
|
||||||
|
(entry.pointChange !== undefined && entry.pointChange !== 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Left: avatar + name */}
|
{/* Left: avatar + name */}
|
||||||
|
|
@ -46,16 +69,44 @@ function RowContent({ entry }: { entry: StandingsPreviewEntry }) {
|
||||||
|
|
||||||
{/* Right: stats — second row on mobile */}
|
{/* Right: stats — second row on mobile */}
|
||||||
<div className="flex items-center gap-4 w-full border-t border-border/30 pt-2 mt-1 sm:w-auto sm:border-0 sm:pt-0 sm:mt-0 sm:shrink-0">
|
<div className="flex items-center gap-4 w-full border-t border-border/30 pt-2 mt-1 sm:w-auto sm:border-0 sm:pt-0 sm:mt-0 sm:shrink-0">
|
||||||
<RankingDisplay displayRank={entry.displayRank} rankChange={entry.rankChange} />
|
<RankingDisplay
|
||||||
|
displayRank={entry.displayRank}
|
||||||
|
rankChange={entry.rankChange}
|
||||||
|
reserveDelta={rowHasChange}
|
||||||
|
/>
|
||||||
|
{showProjected && <StatDivider />}
|
||||||
|
{showProjected && (
|
||||||
|
<StatColumn
|
||||||
|
label="Projected"
|
||||||
|
reserveDelta={rowHasChange}
|
||||||
|
value={
|
||||||
|
hasProjection(entry) ? (
|
||||||
|
<span className="text-2xl font-normal leading-none text-muted-foreground">
|
||||||
|
{Math.round(entry.projectedPoints as number).toLocaleString("en-US")}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-2xl font-normal leading-none text-muted-foreground/40">
|
||||||
|
—
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<StatDivider />
|
<StatDivider />
|
||||||
<StatColumn label="Points">
|
<StatColumn
|
||||||
<span className="text-2xl font-bold leading-none text-electric">
|
label="Points"
|
||||||
{Math.round(entry.points).toLocaleString("en-US")}
|
reserveDelta={rowHasChange}
|
||||||
</span>
|
value={
|
||||||
{entry.pointChange !== undefined && entry.pointChange !== 0 && (
|
<span className="text-2xl font-bold leading-none text-electric">
|
||||||
<PointChangeIndicator delta={entry.pointChange} />
|
{Math.round(entry.points).toLocaleString("en-US")}
|
||||||
)}
|
</span>
|
||||||
</StatColumn>
|
}
|
||||||
|
delta={
|
||||||
|
entry.pointChange !== undefined && entry.pointChange !== 0 ? (
|
||||||
|
<DeltaBadge delta={entry.pointChange} kind="points" />
|
||||||
|
) : undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
@ -81,15 +132,26 @@ export interface StandingsPreviewEntry {
|
||||||
rankChange?: number;
|
rankChange?: number;
|
||||||
/** 7-day point delta. From TeamStanding.sevenDayPointChange. */
|
/** 7-day point delta. From TeamStanding.sevenDayPointChange. */
|
||||||
pointChange?: number;
|
pointChange?: number;
|
||||||
|
/** Projected final points. From TeamStanding.projectedPoints. */
|
||||||
|
projectedPoints?: number | null;
|
||||||
|
/** Live participants left; projection only shown while > 0. */
|
||||||
|
participantsRemaining?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StandingsPreviewProps {
|
export interface StandingsPreviewProps {
|
||||||
entries: StandingsPreviewEntry[];
|
entries: StandingsPreviewEntry[];
|
||||||
description?: string;
|
description?: string;
|
||||||
fullStandingsHref?: string;
|
fullStandingsHref?: string;
|
||||||
|
/** When true, adds a "Projected" column (projected final points). Default false. */
|
||||||
|
showProjected?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function StandingsPreview({ entries, description, fullStandingsHref }: StandingsPreviewProps) {
|
export function StandingsPreview({
|
||||||
|
entries,
|
||||||
|
description,
|
||||||
|
fullStandingsHref,
|
||||||
|
showProjected = false,
|
||||||
|
}: StandingsPreviewProps) {
|
||||||
return (
|
return (
|
||||||
<Card className="gap-2">
|
<Card className="gap-2">
|
||||||
<CardHeader className="px-3 sm:px-6 pb-2">
|
<CardHeader className="px-3 sm:px-6 pb-2">
|
||||||
|
|
@ -119,11 +181,11 @@ export function StandingsPreview({ entries, description, fullStandingsHref }: St
|
||||||
to={entry.href}
|
to={entry.href}
|
||||||
className={rowClasses(entry.currentRank, true)}
|
className={rowClasses(entry.currentRank, true)}
|
||||||
>
|
>
|
||||||
<RowContent entry={entry} />
|
<RowContent entry={entry} showProjected={showProjected} />
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
<div key={entry.teamId} className={rowClasses(entry.currentRank, false)}>
|
<div key={entry.teamId} className={rowClasses(entry.currentRank, false)}>
|
||||||
<RowContent entry={entry} />
|
<RowContent entry={entry} showProjected={showProjected} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,71 @@
|
||||||
|
/**
|
||||||
|
* Stacked stat column: label / value / delta. The delta slot is always rendered
|
||||||
|
* (a muted "—" placeholder when there is no change) so that every row reserves the
|
||||||
|
* same vertical space and the columns line up across the standings table.
|
||||||
|
*/
|
||||||
export function StatColumn({
|
export function StatColumn({
|
||||||
label,
|
label,
|
||||||
children,
|
value,
|
||||||
|
delta,
|
||||||
|
reserveDelta = false,
|
||||||
}: {
|
}: {
|
||||||
label: string;
|
label: string;
|
||||||
children: React.ReactNode;
|
value: React.ReactNode;
|
||||||
|
/** Optional change indicator rendered on its own line below the value. */
|
||||||
|
delta?: React.ReactNode;
|
||||||
|
/**
|
||||||
|
* When true, the delta line is always rendered (a muted placeholder when there is
|
||||||
|
* no delta) so columns stay vertically aligned across rows. When false and no delta
|
||||||
|
* is provided, the line is omitted entirely.
|
||||||
|
*/
|
||||||
|
reserveDelta?: boolean;
|
||||||
}) {
|
}) {
|
||||||
|
const showDeltaLine = delta !== undefined || reserveDelta;
|
||||||
return (
|
return (
|
||||||
<div className="text-right shrink-0 flex-1 sm:flex-none">
|
<div className="text-right shrink-0 flex-1 sm:flex-none">
|
||||||
<p className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
<p className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
||||||
{label}
|
{label}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex items-baseline justify-end gap-1">{children}</div>
|
<div className="flex items-baseline justify-end">{value}</div>
|
||||||
|
{showDeltaLine && (
|
||||||
|
<div className="flex items-baseline justify-end leading-none mt-0.5">
|
||||||
|
{delta ?? <span className="text-xs text-muted-foreground/60">—</span>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function StatDivider() {
|
export function StatDivider({ className = "h-12" }: { className?: string } = {}) {
|
||||||
return <div className="h-8 w-px bg-border shrink-0 self-center" />;
|
return <div className={`${className} w-px bg-border shrink-0 self-center`} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RankChangeIndicator({ delta }: { delta: number }) {
|
/**
|
||||||
if (delta === 0) return null;
|
* 7-day change indicator. `rank` deltas are unitless places; `points` deltas are
|
||||||
if (delta > 0) {
|
* rounded point totals. Positive = up (green ▲), negative = down (coral ▼).
|
||||||
|
*/
|
||||||
|
export function DeltaBadge({
|
||||||
|
delta,
|
||||||
|
kind,
|
||||||
|
}: {
|
||||||
|
delta: number;
|
||||||
|
kind: "rank" | "points";
|
||||||
|
}) {
|
||||||
|
const magnitude = kind === "rank" ? Math.abs(delta) : Math.abs(Math.round(delta));
|
||||||
|
const up = delta > 0;
|
||||||
|
const label =
|
||||||
|
kind === "rank"
|
||||||
|
? up
|
||||||
|
? `up ${magnitude}`
|
||||||
|
: `down ${magnitude}`
|
||||||
|
: up
|
||||||
|
? `+${magnitude} points`
|
||||||
|
: `-${magnitude} points`;
|
||||||
|
|
||||||
|
if (up) {
|
||||||
return (
|
return (
|
||||||
<span className="text-xs font-semibold text-primary" aria-label={`up ${delta}`}>
|
<span className="text-xs font-semibold text-primary" aria-label={label}>
|
||||||
▲{delta}
|
▲{magnitude}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -32,9 +73,9 @@ export function RankChangeIndicator({ delta }: { delta: number }) {
|
||||||
<span
|
<span
|
||||||
className="text-xs font-semibold"
|
className="text-xs font-semibold"
|
||||||
style={{ color: "var(--coral-accent, #ef4444)" }}
|
style={{ color: "var(--coral-accent, #ef4444)" }}
|
||||||
aria-label={`down ${Math.abs(delta)}`}
|
aria-label={label}
|
||||||
>
|
>
|
||||||
▼{Math.abs(delta)}
|
▼{magnitude}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -42,35 +83,22 @@ export function RankChangeIndicator({ delta }: { delta: number }) {
|
||||||
export function RankingDisplay({
|
export function RankingDisplay({
|
||||||
displayRank,
|
displayRank,
|
||||||
rankChange,
|
rankChange,
|
||||||
|
reserveDelta = false,
|
||||||
}: {
|
}: {
|
||||||
displayRank: string | number;
|
displayRank: string | number;
|
||||||
rankChange?: number;
|
rankChange?: number;
|
||||||
|
reserveDelta?: boolean;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<StatColumn label="Ranking">
|
<StatColumn
|
||||||
<span className="text-2xl font-bold leading-none">{displayRank}</span>
|
label="Ranking"
|
||||||
{rankChange !== undefined && rankChange !== 0 && (
|
reserveDelta={reserveDelta}
|
||||||
<RankChangeIndicator delta={rankChange} />
|
value={<span className="text-2xl font-bold leading-none">{displayRank}</span>}
|
||||||
)}
|
delta={
|
||||||
</StatColumn>
|
rankChange !== undefined && rankChange !== 0 ? (
|
||||||
);
|
<DeltaBadge delta={rankChange} kind="rank" />
|
||||||
}
|
) : undefined
|
||||||
|
}
|
||||||
export function PointChangeIndicator({ delta }: { delta: number }) {
|
/>
|
||||||
if (delta >= 0) {
|
|
||||||
return (
|
|
||||||
<span className="text-xs font-semibold text-primary" aria-label={`+${Math.round(delta)} points`}>
|
|
||||||
▲{Math.round(delta)}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
className="text-xs font-semibold"
|
|
||||||
style={{ color: "var(--coral-accent, #ef4444)" }}
|
|
||||||
aria-label={`${Math.round(delta)} points`}
|
|
||||||
>
|
|
||||||
▼{Math.abs(Math.round(delta))}
|
|
||||||
</span>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -103,4 +103,54 @@ describe("StandingsPreview", () => {
|
||||||
expect(link).toHaveAttribute("href", "/leagues/1/standings/1");
|
expect(link).toHaveAttribute("href", "/leagues/1/standings/1");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Projections", () => {
|
||||||
|
it("does not render a Projected column by default", () => {
|
||||||
|
const entry: StandingsPreviewEntry = {
|
||||||
|
...baseEntry,
|
||||||
|
projectedPoints: 3000,
|
||||||
|
participantsRemaining: 3,
|
||||||
|
};
|
||||||
|
renderWithRouter(<StandingsPreview entries={[entry]} />);
|
||||||
|
|
||||||
|
expect(screen.queryByText("Projected")).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByText("3,000")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders the projected value when showProjected and participants remain", () => {
|
||||||
|
const entry: StandingsPreviewEntry = {
|
||||||
|
...baseEntry,
|
||||||
|
projectedPoints: 3000,
|
||||||
|
participantsRemaining: 3,
|
||||||
|
};
|
||||||
|
renderWithRouter(<StandingsPreview entries={[entry]} showProjected />);
|
||||||
|
|
||||||
|
expect(screen.getByText("Projected")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("3,000")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows a placeholder instead of a value when no participants remain", () => {
|
||||||
|
const entry: StandingsPreviewEntry = {
|
||||||
|
...baseEntry,
|
||||||
|
projectedPoints: 9999,
|
||||||
|
participantsRemaining: 0,
|
||||||
|
};
|
||||||
|
renderWithRouter(<StandingsPreview entries={[entry]} showProjected />);
|
||||||
|
|
||||||
|
// Column header still present, but the projected total is not shown.
|
||||||
|
expect(screen.getByText("Projected")).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText("9,999")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows a placeholder when projectedPoints is missing", () => {
|
||||||
|
const entry: StandingsPreviewEntry = {
|
||||||
|
...baseEntry,
|
||||||
|
projectedPoints: null,
|
||||||
|
participantsRemaining: 3,
|
||||||
|
};
|
||||||
|
renderWithRouter(<StandingsPreview entries={[entry]} showProjected />);
|
||||||
|
|
||||||
|
expect(screen.getByText("Projected")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import {
|
||||||
findDraftSlotsBySeasonId,
|
findDraftSlotsBySeasonId,
|
||||||
} from "~/models";
|
} from "~/models";
|
||||||
import { findCurrentSeasonWithSports } from "~/models/season";
|
import { findCurrentSeasonWithSports } from "~/models/season";
|
||||||
import { getSeasonStandings } from "~/models/standings";
|
import { getSevenDayStandingsChange } from "~/models/standings";
|
||||||
import { getUpcomingEventsForDraftedParticipants } from "~/models/scoring-event";
|
import { getUpcomingEventsForDraftedParticipants } from "~/models/scoring-event";
|
||||||
import { getUpcomingGroupStageMatchesForParticipants } from "~/models/group-stage-match";
|
import { getUpcomingGroupStageMatchesForParticipants } from "~/models/group-stage-match";
|
||||||
import { getDraftedParticipantsBySportsSeason, getDraftedParticipantsWithPoints, type DraftedParticipantWithPoints } from "~/models/draft-pick";
|
import { getDraftedParticipantsBySportsSeason, getDraftedParticipantsWithPoints, type DraftedParticipantWithPoints } from "~/models/draft-pick";
|
||||||
|
|
@ -77,7 +77,7 @@ export async function loader(args: Route.LoaderArgs) {
|
||||||
// Fetch standings for active/completed seasons
|
// Fetch standings for active/completed seasons
|
||||||
const standings =
|
const standings =
|
||||||
season && (season.status === "active" || season.status === "completed")
|
season && (season.status === "active" || season.status === "completed")
|
||||||
? await getSeasonStandings(season.id)
|
? await getSevenDayStandingsChange(season.id)
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
// Batch-fetch all users needed for owner and commissioner maps in one query
|
// Batch-fetch all users needed for owner and commissioner maps in one query
|
||||||
|
|
|
||||||
|
|
@ -140,6 +140,8 @@ export default function LeagueStandings() {
|
||||||
points: s.totalPoints,
|
points: s.totalPoints,
|
||||||
rankChange: s.rankChange,
|
rankChange: s.rankChange,
|
||||||
pointChange: s.sevenDayPointChange,
|
pointChange: s.sevenDayPointChange,
|
||||||
|
projectedPoints: s.projectedPoints,
|
||||||
|
participantsRemaining: s.participantsRemaining,
|
||||||
href: `/leagues/${league.id}/standings/${season.id}/teams/${s.teamId}`,
|
href: `/leagues/${league.id}/standings/${season.id}/teams/${s.teamId}`,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
@ -181,6 +183,7 @@ export default function LeagueStandings() {
|
||||||
<StandingsPreview
|
<StandingsPreview
|
||||||
entries={previewEntries}
|
entries={previewEntries}
|
||||||
description={seasonComplete ? "Final Standings" : "Current Standings"}
|
description={seasonComplete ? "Final Standings" : "Current Standings"}
|
||||||
|
showProjected={!seasonComplete}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{progressionData.chartData.length > 0 && (
|
{progressionData.chartData.length > 0 && (
|
||||||
|
|
|
||||||
|
|
@ -116,7 +116,7 @@ export default function LeagueHome({ loaderData }: Route.ComponentProps) {
|
||||||
currentRank: standing?.currentRank,
|
currentRank: standing?.currentRank,
|
||||||
points: standing ? (standing.actualPoints ?? standing.totalPoints) : 0,
|
points: standing ? (standing.actualPoints ?? standing.totalPoints) : 0,
|
||||||
href: season ? `/leagues/${league.id}/standings/${season.id}/teams/${team.id}` : undefined,
|
href: season ? `/leagues/${league.id}/standings/${season.id}/teams/${team.id}` : undefined,
|
||||||
rankChange: standing?.rankChange,
|
rankChange: standing?.sevenDayRankChange,
|
||||||
pointChange: standing?.sevenDayPointChange,
|
pointChange: standing?.sevenDayPointChange,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue