/* ============================================================ VYNDR — OUTLOOK MODE (Wave 4A, Step 3): the never-empty slate grid. The game grid must NEVER dead-end in a "NO SLATE" CTA. When there are no live games (and it's not a network failure) the grid falls back to REAL, always-available data — the terminal is never dark, but it is never fabricated either: • yesterday's PROVEN A-tier receipts (settled ledger hits), and/or • tomorrow's date-pinned ESPN schedule preview (free / cached). A network `fetchError` is DISTINCT and stays an ERROR state — a fetch failure is never dressed up as a fake outlook. Plain CommonJS so the .tsx surfaces import it (allowJs) AND Jest exercises the selection logic directly. ============================================================ */ const { buildHeroReceipts } = require('./slateAdapter'); /** Statuses that mean a game is no longer a PREVIEW (already underway/done). */ const NON_PREVIEW = new Set(['in', 'post', 'final', 'live', 'completed']); /** * mapTomorrowPreview(scheduleGames, limit) — map real schedule games (either * the ESPN `{ awayTeam, homeTeam, gameTime, status }` shape OR the flattened * `{ away, home, start_time }` shape) → compact preview rows. Upcoming only; * a game missing either team is DROPPED (never fabricate a matchup). */ function mapTomorrowPreview(scheduleGames, limit = 8) { const list = Array.isArray(scheduleGames) ? scheduleGames : []; const out = []; for (const g of list) { if (!g) continue; const status = String(g.status || g.state || '').toLowerCase(); if (NON_PREVIEW.has(status)) continue; const away = g.away || g.awayTeam?.name || g.awayTeam?.abbreviation || null; const home = g.home || g.homeTeam?.name || g.homeTeam?.abbreviation || null; if (!away || !home) continue; // absent beats a fabricated fixture out.push({ id: g.id || `${away}-${home}`, away, home, time: g.gameTime || g.start_time || g.time || null, sport: g.sport ? String(g.sport).toUpperCase() : null, }); if (out.length >= Math.max(0, limit)) break; } return out; } /** * buildOutlook(opts) → * { mode: 'error' } — a network failure * { mode: 'live' } — real games on the board * { mode: 'outlook', receipts, tomorrow, hasContent } * * The outlook surface is ALWAYS non-error / non-blank at the surface level: * even with no receipts and no schedule, the caller still renders the * month-aware empty-state header — the grid never dead-ends in a CTA. * * @param {{ gamesCount?: number, fetchError?: unknown, settledRows?: unknown[], tomorrow?: unknown[] }} [opts] */ function buildOutlook(opts = {}) { const { gamesCount = 0, fetchError = null, settledRows = [], tomorrow = [] } = opts; if (fetchError) return { mode: 'error' }; if (Number(gamesCount) > 0) return { mode: 'live' }; const receipts = buildHeroReceipts(settledRows, 6); const preview = mapTomorrowPreview(tomorrow, 8); return { mode: 'outlook', receipts, tomorrow: preview, hasContent: receipts.length > 0 || preview.length > 0, }; } module.exports = { buildOutlook, mapTomorrowPreview };