M1b screen 01: the flat EDGE BOARD — Design's mobile board IA
The one genuinely-new mobile screen. Design's mobile BOARD is a FLAT edge-ranked list (all graded props across every game on one list, sorted by edge) — not the desktop's game-grouped cards. Implemented to the drawing with REAL snapshot data: - slateAdapter.flattenToEdgeBoard(cards) — pure transform of the assembled GameCardData[] (grade→game join already done) into ranked rows, edge desc. STRICT null edge sorts LAST (never 0-coerced to the top — Data Semantics Rule). Threaded edge_pct through buildPlayerStripsFromProps (was dropped). 6 unit tests. - MobileEdgeBoard component — Design's exact screen-01 rows: rank (green #1), player + prop, matchup sub-line with TeamChips + live-dot, tier grade chip, and the edge% as the one bold mono hero (green +, red −). Ranked opacity ramp (1 → .55) + green inset border on the top reads. Breadth strip EDGES/AVG CLV/ GAMES — CLV honest '—' (per-slate CLV isn't computed; never fabricated). - Slate: <768px renders the flat board, ≥768px keeps game cards (same data, toggled by width). Ungraded slate still shows game cards on phones (no blank). Built to Design's screen-01 drawing, VISUALLY UNVERIFIED at 390px — the core mobile screen, top of the master-audit list. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
'use strict';
|
||||
|
||||
// Rev 3 — flat EDGE BOARD (mobile screen 01). Design's mobile board is a flat,
|
||||
// edge-ranked list across every game, not game-grouped cards. The flatten is a
|
||||
// pure transform of the assembled GameCardData[]; lock its ranking + honesty.
|
||||
|
||||
const { flattenToEdgeBoard, edgeBoardBreadth, edgeGradeRank } = require('../../web/src/lib/slateAdapter');
|
||||
|
||||
const card = (over) => ({
|
||||
sport: 'nba', live: false,
|
||||
away: { abbr: 'DEN', name: 'Denver' }, home: { abbr: 'MIN', name: 'Minnesota' },
|
||||
playerStrips: [], ...over,
|
||||
});
|
||||
const strip = (player, props, extra = {}) => ({ player, team: extra.team || '', props });
|
||||
|
||||
describe('flattenToEdgeBoard', () => {
|
||||
const cards = [
|
||||
card({ playerStrips: [
|
||||
strip('Jokic', [{ stat: 'PTS', line: 27.5, side: 'O', grade: 'A+', edge: 8.4 }]),
|
||||
strip('Gordon', [{ stat: 'REB', line: 7.5, side: 'O', grade: 'B', edge: 3.4 }]),
|
||||
] }),
|
||||
card({ live: true, away: { abbr: 'CHI', name: 'Chi' }, home: { abbr: 'MIL', name: 'Mil' }, playerStrips: [
|
||||
strip('Edwards', [{ stat: 'AST', line: 5.5, side: 'O', grade: 'A', edge: 5.2 }]),
|
||||
strip('LaVine', [{ stat: 'PTS', line: 22.5, side: 'O', grade: 'D', edge: -1.2 }]),
|
||||
] }),
|
||||
];
|
||||
|
||||
test('flattens every graded prop across games into ranked rows, edge desc', () => {
|
||||
const rows = flattenToEdgeBoard(cards);
|
||||
expect(rows.map((r) => r.player)).toEqual(['Jokic', 'Edwards', 'Gordon', 'LaVine']); // 8.4, 5.2, 3.4, -1.2
|
||||
expect(rows[0].rank).toBe(1);
|
||||
expect(rows[3].rank).toBe(4);
|
||||
expect(rows[1].live).toBe(true); // Edwards' game is live → row carries it
|
||||
expect(rows[0].away).toBe('DEN'); expect(rows[0].home).toBe('MIN');
|
||||
});
|
||||
|
||||
test('a null edge sorts LAST, never 0-coerced to the top', () => {
|
||||
const rows = flattenToEdgeBoard([card({ playerStrips: [
|
||||
strip('NoEdge', [{ stat: 'PTS', line: 20, side: 'O', grade: 'A', edge: null }]),
|
||||
strip('HasEdge', [{ stat: 'PTS', line: 20, side: 'O', grade: 'C', edge: 2.0 }]),
|
||||
] })]);
|
||||
expect(rows[0].player).toBe('HasEdge'); // +2.0 beats a null (not 0)
|
||||
expect(rows[1].player).toBe('NoEdge');
|
||||
});
|
||||
|
||||
test('awaiting / dead / ungraded rows are excluded from the board', () => {
|
||||
const rows = flattenToEdgeBoard([card({ playerStrips: [
|
||||
strip('Graded', [{ stat: 'PTS', line: 20, side: 'O', grade: 'A', edge: 5 }]),
|
||||
strip('Awaiting', [{ stat: 'PTS', line: 20, side: 'O', grade: null, awaiting: true }]),
|
||||
strip('Dead', [{ stat: 'PTS', line: 20, side: 'O', grade: 'B', edge: 4, dead: true }]),
|
||||
] })]);
|
||||
expect(rows.map((r) => r.player)).toEqual(['Graded']);
|
||||
});
|
||||
|
||||
test('breadth counts A/B edges + games, never fabricates CLV', () => {
|
||||
const rows = flattenToEdgeBoard(cards);
|
||||
const b = edgeBoardBreadth(rows, cards);
|
||||
expect(b.edges).toBe(3); // A+, B, A (not the D)
|
||||
expect(b.games).toBe(2);
|
||||
expect(b).not.toHaveProperty('clv'); // CLV comes from /api/accuracy, never invented here
|
||||
});
|
||||
|
||||
test('edgeGradeRank orders tiers for the tiebreak', () => {
|
||||
expect(edgeGradeRank('A+')).toBeLessThan(edgeGradeRank('A'));
|
||||
expect(edgeGradeRank('A')).toBeLessThan(edgeGradeRank('B'));
|
||||
expect(edgeGradeRank('F')).toBeLessThan(edgeGradeRank('ZZZ')); // unknown → last
|
||||
});
|
||||
|
||||
test('empty / malformed input → empty board, no throw', () => {
|
||||
expect(flattenToEdgeBoard(null)).toEqual([]);
|
||||
expect(flattenToEdgeBoard([{}])).toEqual([]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user