Session 51: Complete Team Hub (2234 tests)

Research-depth team view: /team/[abbr] with roster, archetypes, stats, props.

- Team API: mlbStatsAdapter.getTeams/resolveTeam/getTeamRoster (statsapi, abbr→id
  + active roster, cached). teamService.getTeamHub assembles roster → per-player
  season stats (bounded concurrency) + archetype (snapshot grade or classify) +
  tonight's graded props from grades:{sport}; whole hub cached 15min. MLB real;
  NBA/WNBA graceful snapshot roster. GET /api/team/:abbr (404 unknown) + proxy.
- Team Hub page: server page.tsx (generateMetadata) + TeamHub client — header,
  sort (archetype/graded/A-Z), archetype filter chips, roster rows (archetype +
  player link + position + stats + graded props + parlay "+"), "No active props"
  greyed state, loading/error.
- Game cards: team abbreviations are now TeamLinks → /team/:abbr?sport= (green
  hover, stops propagation). Team Hub has "← Back to Slate".

Backend 2215 -> 2234 tests (+19), 190 suites. Web build clean (exit 0).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kev
2026-06-19 11:55:10 -04:00
parent f1956dc953
commit f0674ca07d
14 changed files with 696 additions and 4 deletions
+19 -1
View File
@@ -68,6 +68,24 @@ interface GameCardProps {
preferredBooks?: string[];
}
/** Clickable team abbreviation → /team/:abbr (Session 51). Stops propagation so
* it doesn't trigger the card's open-game handler; green underline on hover. */
function TeamLink({ abbr, sport }: { abbr: string; sport: string }) {
if (!abbr) return <span></span>;
return (
<a
href={`/team/${encodeURIComponent(abbr)}?sport=${encodeURIComponent(sport || 'mlb')}`}
onClick={(e) => e.stopPropagation()}
style={{ color: '#fff', textDecoration: 'none', borderBottom: '1px solid transparent' }}
onMouseEnter={(e) => { e.currentTarget.style.color = 'var(--g-a)'; e.currentTarget.style.borderBottomColor = 'var(--g-a)'; }}
onMouseLeave={(e) => { e.currentTarget.style.color = '#fff'; e.currentTarget.style.borderBottomColor = 'transparent'; }}
title={`${abbr} team hub`}
>
{abbr}
</a>
);
}
/** A book-line cell with the Bloomberg pattern: best = green tint + green left
* border, worst = subtle red. The #1 visual upgrade (§13). */
function LineCell({ value, best, worst }: { value: string; best?: boolean; worst?: boolean }) {
@@ -148,7 +166,7 @@ export default function GameCard({ game: g, onAddParlay, onOpen, preferredBooks
<div onClick={() => onOpen && onOpen(g.id)} title="Open game detail" style={{ display: 'flex', alignItems: 'center', gap: 11, minWidth: 0, cursor: onOpen ? 'pointer' : 'default' }}>
<SportBadge sport={g.sport} />
<span className="mono" style={{ fontSize: 18, fontWeight: 700, letterSpacing: '0.01em' }}>
{g.away.abbr} <span style={{ color: 'var(--text-2)', fontWeight: 400 }}>@</span> {g.home.abbr}
<TeamLink abbr={g.away.abbr} sport={g.sport} /> <span style={{ color: 'var(--text-2)', fontWeight: 400 }}>@</span> <TeamLink abbr={g.home.abbr} sport={g.sport} />
</span>
{g.live && (
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 6, marginLeft: 2 }}>