Session 36: Design system Phase E — remaining screens + dashboard Bloomberg lines (1853 tests)
VYNDR 2.0 conversion, Phase E. Frontend-only; zero backend changes. - lib/slateAdapter.js: parseAmericanOdds, detectBestLines, mapScheduleToGameCards (best/worst line detection — the Bloomberg pattern). - Reskinned the LEGACY GameCard's game-lines grid with best/worst highlighting + SportBadge, keeping inline grading intact (a wholesale swap to the display-only vyndr/GameCard would have deleted the slate's grading interaction). - compare/invite/help/about: RouteStubs -> real design-system pages. - login reskinned (scanlines, system voice, new Wordmark); pricing + ClaimMeter. Honest scope: the full GameCard swap needs inline grading ported into the new component first; profile/settings/blog/game-detail reskins are light/deferred. 18 new tests. Backend 1839 -> 1853, 144 suites, zero regressions. Web build clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
/* ============================================================
|
||||
VYNDR 2.0 — slate adapter (§7, §E.1).
|
||||
Merges schedule + gamelines + streaks + grades into the GameCard
|
||||
contract, and detects the best/worst book line per game (the
|
||||
Bloomberg pattern — the #1 visual upgrade). Plain CommonJS so the
|
||||
.tsx cards import it (allowJs) AND Jest exercises the logic directly.
|
||||
============================================================ */
|
||||
|
||||
/** American odds → decimal payout multiplier (higher = better for the bettor).
|
||||
* "+150" → 2.5, "-110" → ~1.909. Returns null when unparseable. */
|
||||
function parseAmericanOdds(odds) {
|
||||
if (odds == null) return null;
|
||||
const n = typeof odds === 'number' ? odds : parseInt(String(odds).replace(/[^\d+-]/g, ''), 10);
|
||||
if (!Number.isFinite(n) || n === 0) return null;
|
||||
return n > 0 ? 1 + n / 100 : 1 + 100 / Math.abs(n);
|
||||
}
|
||||
|
||||
/** Mark the most/least favorable moneyline per side across a game's books.
|
||||
* Input: { book1: { awayML, homeML, total }, ... } → rows with best/worst flags.
|
||||
* best/worst only set when ≥2 books disagree (a lone price isn't "best"). */
|
||||
function detectBestLines(books) {
|
||||
const entries = Object.entries(books || {});
|
||||
const rows = entries.map(([book, ln]) => ({
|
||||
book,
|
||||
awayML: ln.awayML || '—',
|
||||
homeML: ln.homeML || '—',
|
||||
ou: ln.total != null ? `O/U ${ln.total}` : '—',
|
||||
_away: parseAmericanOdds(ln.awayML),
|
||||
_home: parseAmericanOdds(ln.homeML),
|
||||
}));
|
||||
|
||||
const mark = (side) => {
|
||||
const vals = rows.map((r) => r[side]).filter((v) => v != null);
|
||||
if (vals.length < 2) return [null, null];
|
||||
const max = Math.max(...vals);
|
||||
const min = Math.min(...vals);
|
||||
return max === min ? [null, null] : [max, min];
|
||||
};
|
||||
const [bestAway, worstAway] = mark('_away');
|
||||
const [bestHome, worstHome] = mark('_home');
|
||||
|
||||
return rows.map((r) => ({
|
||||
book: r.book,
|
||||
awayML: r.awayML,
|
||||
homeML: r.homeML,
|
||||
ou: r.ou,
|
||||
bestAway: r._away != null && r._away === bestAway,
|
||||
worstAway: r._away != null && r._away === worstAway,
|
||||
bestHome: r._home != null && r._home === bestHome,
|
||||
worstHome: r._home != null && r._home === worstHome,
|
||||
}));
|
||||
}
|
||||
|
||||
function formatGameTime(iso) {
|
||||
if (!iso) return '';
|
||||
try {
|
||||
return new Date(iso).toLocaleString(undefined, { weekday: 'short', hour: 'numeric', minute: '2-digit' });
|
||||
} catch {
|
||||
return String(iso);
|
||||
}
|
||||
}
|
||||
|
||||
/** Map one schedule game's lines entry → the GameCard `lines[]` contract. */
|
||||
function mapGameLines(linesEntry) {
|
||||
if (!linesEntry || !linesEntry.books) return [];
|
||||
return detectBestLines(linesEntry.books);
|
||||
}
|
||||
|
||||
/**
|
||||
* Map schedule + gamelines + streaks (+ optional grades) → GameCardData[]
|
||||
* (§7). Pure — no API calls, no side effects.
|
||||
*/
|
||||
function mapScheduleToGameCards(schedule, gamelines, streaks, grades) {
|
||||
const sched = Array.isArray(schedule) ? schedule : [];
|
||||
return sched.map((g) => {
|
||||
const id = g.id || `${g.awayTeam?.abbreviation || '?'}-${g.homeTeam?.abbreviation || '?'}`;
|
||||
const live = g.live === true || g.status === 'in';
|
||||
const linesEntry = gamelines && gamelines[id];
|
||||
return {
|
||||
id,
|
||||
sport: (g.sport || 'nba').toLowerCase(),
|
||||
live,
|
||||
score: g.score ? { away: g.score.away, home: g.score.home } : undefined,
|
||||
clock: g.clock || undefined,
|
||||
away: { abbr: g.awayTeam?.abbreviation || '', name: g.awayTeam?.name || '' },
|
||||
home: { abbr: g.homeTeam?.abbreviation || '', name: g.homeTeam?.name || '' },
|
||||
time: formatGameTime(g.gameTime),
|
||||
venue: g.venue || undefined,
|
||||
lines: mapGameLines(linesEntry),
|
||||
props: mapGradedProps(grades, g),
|
||||
streaks: mapStreaks(streaks, g),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function mapGradedProps(grades, game) {
|
||||
if (!Array.isArray(grades)) return [];
|
||||
const h = (game.homeTeam?.abbreviation || '').toUpperCase();
|
||||
const a = (game.awayTeam?.abbreviation || '').toUpperCase();
|
||||
return grades
|
||||
.filter((p) => {
|
||||
const t = (p.team || '').toUpperCase();
|
||||
return !t || t === h || t === a;
|
||||
})
|
||||
.map((p) => ({ player: p.player, stat: p.stat, line: p.line, grade: p.grade, side: p.side || 'Over', delta: p.delta }));
|
||||
}
|
||||
|
||||
function mapStreaks(streaks, game) {
|
||||
if (!Array.isArray(streaks)) return [];
|
||||
const h = (game.homeTeam?.abbreviation || '').toUpperCase();
|
||||
const a = (game.awayTeam?.abbreviation || '').toUpperCase();
|
||||
return streaks
|
||||
.filter((s) => {
|
||||
const t = (s.team || '').toUpperCase();
|
||||
return t && (t === h || t === a);
|
||||
})
|
||||
.map((s) => ({ player: s.player, text: s.text || s.description || '' }));
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
parseAmericanOdds,
|
||||
detectBestLines,
|
||||
mapGameLines,
|
||||
mapScheduleToGameCards,
|
||||
formatGameTime,
|
||||
};
|
||||
Reference in New Issue
Block a user