Wave 2B: Offseason never-dark hub — NewsWire + FuturesBoard on /explore
Extend /explore (ExploreHub) into the 365-day never-dark hub. Two new self-hiding sections feed REAL always-available data into the offseason: - NewsWire (components/vyndr/NewsWire.tsx): real ESPN headlines from /api/news/:sport (newest-first, mono timestamps, type chips, player/team links) + real injuries from /api/schedule/:sport/injuries (OUT/GTD chips, token colors). Reuses the retired TerminalTemplates INJURY_WIRE layout but never routes its sample constants. Self-hides when both feeds are empty. - FuturesBoard (components/vyndr/FuturesBoard.tsx): real futures from /api/futures/:sport — championship/win-total/award markets, mono tabular prices + movement colored by the contract (shortening=green / drifting=amber / flat=dim, NEVER red; move shown ONLY when the backend supplies one). Carries the honest "TRACKED · NOT GRADED" label — no fabricated grades on futures. Self-hides when markets:[]. - ExploreHub is offseason-aware (via emptyState OFF_SEASON month check): the hub LEADS with futures + wire when the board is dark, COMPLEMENTS the live board in-season. Sport selector kept; each section self-hides independently. - Testable pure helpers: lib/futuresMove.js (move→color, never red) + lib/newsFormat.js (timeAgo mono-stamp, ESPN type labels). Contracts consumed (Wave 2A owns the proxy/service files); code self-hides on fetch failure if a proxy isn't present yet. Tests: tests/unit/newsWire.test.js + futuresBoard.test.js (26 new). Full suite 259 suites / 3144 green; web build exit 0. vyndrParityQA stays green (mono data, no glitch on data surfaces). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
/* ============================================================
|
||||
VYNDR — FUTURES MOVEMENT color/label (Wave 2B, Offseason Hub).
|
||||
|
||||
The color contract for a futures selection's price movement. HONESTY RULE:
|
||||
futures are TRACKED, not model-graded — a movement is only ever shown when
|
||||
the backend actually supplies a `move` (never fabricated). The color space
|
||||
is deliberately narrow and NEVER red:
|
||||
• shortening → the market moved TOWARD the selection → green (value/steam)
|
||||
• drifting → the market moved AWAY → amber (caution)
|
||||
• flat / absent → no movement to report → dim (say less)
|
||||
Red is reserved system-wide for settled-negative outcomes; a futures drift
|
||||
is NOT a miss, so it must never render red.
|
||||
|
||||
CommonJS so the .tsx surface imports it (allowJs) AND Jest exercises it.
|
||||
============================================================ */
|
||||
|
||||
// Canonical move → CSS custom-property token. No branch returns the red
|
||||
// (--miss) token — a normal drift is caution (amber), never a miss.
|
||||
const MOVE_COLOR = {
|
||||
shortening: 'var(--g-a)', // green — toward the selection (value)
|
||||
drifting: 'var(--amber)', // amber — away from the selection (caution)
|
||||
flat: 'var(--text-2)', // dim — no movement
|
||||
};
|
||||
|
||||
/** moveColor(move) — the token for a move, defaulting to dim for flat/absent. */
|
||||
function moveColor(move) {
|
||||
const m = String(move || '').toLowerCase();
|
||||
return MOVE_COLOR[m] || MOVE_COLOR.flat;
|
||||
}
|
||||
|
||||
/** moveLabel(move) — a short mono glyph+word, or '' when there is no move. */
|
||||
function moveLabel(move) {
|
||||
const m = String(move || '').toLowerCase();
|
||||
if (m === 'shortening') return '▼ SHORTENING';
|
||||
if (m === 'drifting') return '▲ DRIFTING';
|
||||
if (m === 'flat') return '— FLAT';
|
||||
return '';
|
||||
}
|
||||
|
||||
module.exports = { MOVE_COLOR, moveColor, moveLabel };
|
||||
@@ -0,0 +1,53 @@
|
||||
/* ============================================================
|
||||
VYNDR — NEWS WIRE formatting (Wave 2B, Offseason Hub).
|
||||
|
||||
Small pure helpers for the real-ESPN news wire. Timestamps are rendered in
|
||||
mono (the brand rule: all data is mono); an unparseable/absent published
|
||||
time yields '' (absent beats a fabricated "just now"). Type chips map the
|
||||
raw ESPN feed type to a short human label without inventing categories.
|
||||
|
||||
CommonJS so the .tsx surface imports it (allowJs) AND Jest exercises it.
|
||||
============================================================ */
|
||||
|
||||
/**
|
||||
* timeAgo(published, now) — compact relative time for a mono timestamp.
|
||||
* Invalid / missing input → '' (never a fabricated stamp). `published` is an
|
||||
* ISO string or epoch ms; `now` defaults to Date.now().
|
||||
*/
|
||||
function timeAgo(published, now = Date.now()) {
|
||||
if (published == null || published === '') return '';
|
||||
const t = typeof published === 'number' ? published : Date.parse(String(published));
|
||||
if (!Number.isFinite(t)) return '';
|
||||
const diff = Number(now) - t;
|
||||
if (!Number.isFinite(diff)) return '';
|
||||
if (diff < 0) return 'now';
|
||||
const min = Math.floor(diff / 60000);
|
||||
if (min < 1) return 'now';
|
||||
if (min < 60) return `${min}m ago`;
|
||||
const hr = Math.floor(min / 60);
|
||||
if (hr < 24) return `${hr}h ago`;
|
||||
const day = Math.floor(hr / 24);
|
||||
return `${day}d ago`;
|
||||
}
|
||||
|
||||
// Raw ESPN feed types → short display labels. Unknown types fall back to a
|
||||
// title-cased version of the raw string (never dropped, never invented).
|
||||
const TYPE_LABEL = {
|
||||
Recap: 'RECAP',
|
||||
HeadlineNews: 'NEWS',
|
||||
Story: 'STORY',
|
||||
Preview: 'PREVIEW',
|
||||
Notebook: 'NOTEBOOK',
|
||||
Media: 'MEDIA',
|
||||
};
|
||||
|
||||
/** typeLabel(type) — short uppercase chip label, or '' when absent. */
|
||||
function typeLabel(type) {
|
||||
if (!type) return '';
|
||||
const raw = String(type);
|
||||
if (TYPE_LABEL[raw]) return TYPE_LABEL[raw];
|
||||
// split camelCase / snake and uppercase
|
||||
return raw.replace(/([a-z])([A-Z])/g, '$1 $2').replace(/[_-]+/g, ' ').trim().toUpperCase();
|
||||
}
|
||||
|
||||
module.exports = { timeAgo, typeLabel, TYPE_LABEL };
|
||||
Reference in New Issue
Block a user