brackt/app/components/league/StatHelpers.tsx
Chris Parsons b960d39be3
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
Add projections to full standings; clean up change indicators
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>
2026-06-21 11:56:15 -07:00

104 lines
2.8 KiB
TypeScript

/**
* 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({
label,
value,
delta,
reserveDelta = false,
}: {
label: string;
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 (
<div className="text-right shrink-0 flex-1 sm:flex-none">
<p className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
{label}
</p>
<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>
);
}
export function StatDivider({ className = "h-12" }: { className?: string } = {}) {
return <div className={`${className} w-px bg-border shrink-0 self-center`} />;
}
/**
* 7-day change indicator. `rank` deltas are unitless places; `points` deltas are
* 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 (
<span className="text-xs font-semibold text-primary" aria-label={label}>
{magnitude}
</span>
);
}
return (
<span
className="text-xs font-semibold"
style={{ color: "var(--coral-accent, #ef4444)" }}
aria-label={label}
>
{magnitude}
</span>
);
}
export function RankingDisplay({
displayRank,
rankChange,
reserveDelta = false,
}: {
displayRank: string | number;
rankChange?: number;
reserveDelta?: boolean;
}) {
return (
<StatColumn
label="Ranking"
reserveDelta={reserveDelta}
value={<span className="text-2xl font-bold leading-none">{displayRank}</span>}
delta={
rankChange !== undefined && rankChange !== 0 ? (
<DeltaBadge delta={rankChange} kind="rank" />
) : undefined
}
/>
);
}