Item 6 — Desk showcase renders REAL data (or hides), kills the mocked ladder
The pricing Desk showcase hardcoded an alt-line ladder (1.5 A +7.1% / 2.5 A+ +11.4% / 4.5 C -3.8%), QUARTER-KELLY 2.4%, and PARLAY φ 0.34 — a mocked demo selling something we weren't proving. - deskShowcaseService reads the pre-graded snapshot for a real A/B prop's alt-line ladder (prefers the one with the most grade variation — the most compelling real example). Edge per rung shows only when it's a plausible market value; the inflated (model-line)/line artifact on small lines is guarded to "—" rather than shown as a fake +91%. - PARLAY φ is now REAL: the model's same-team correlation (0.34, mirroring the frontend parlayMath team constant) computed for TWO REAL same-team legs, named. No real same-team pair on the board → the tile hides, never an invented number. - QUARTER-KELLY tile is REMOVED: the snapshot has no odds, so a real quarter-Kelly % can't be computed here — a fabricated 2.4% is worse than nothing. Kelly stays a real in-app Desk feature; the showcase just doesn't fake it. - DeskShowcase is now a client component fetching /api/desk-showcase; when the board has no real ladder the whole visuals column hides (real-or-hidden, same law as the hero). The pitch copy is unchanged. 5 service tests. Change-affected suites green, web build exit 0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:3000';
|
||||
|
||||
/**
|
||||
* Desk-showcase proxy (item 6) — real alt-line ladder + same-team correlation
|
||||
* from the snapshot. Any failure → { available:false } so the visuals hide.
|
||||
*/
|
||||
export async function GET() {
|
||||
try {
|
||||
const upstream = await fetch(`${BACKEND_URL}/api/desk-showcase`, {
|
||||
method: 'GET', headers: { Accept: 'application/json' }, cache: 'no-store',
|
||||
});
|
||||
const data = await upstream.json().catch(() => ({ available: false }));
|
||||
return NextResponse.json(data, { status: 200, headers: { 'Cache-Control': 'public, s-maxage=300, stale-while-revalidate=60' } });
|
||||
} catch {
|
||||
return NextResponse.json({ available: false }, { status: 200 });
|
||||
}
|
||||
}
|
||||
@@ -1,32 +1,64 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import SectionHead from '@/components/vyndr/SectionHead';
|
||||
|
||||
/**
|
||||
* DeskShowcase (DS5 · DESIGN-SPEC Part 6, #8) — the Desk-as-hero story that
|
||||
* sits ABOVE the pricing grid. Its job is the founder's one line: make $44.99
|
||||
* feel impossibly low for a professional terminal ("how is this only $44.99"
|
||||
* IS the conversion event). Balanced two-column layout — the pitch on the left,
|
||||
* REAL feature visuals on the right (kills the dead right-half, audit #8). The
|
||||
* single primary CTA scrolls to the grid where the real Stripe checkout lives
|
||||
* (checkout wiring untouched).
|
||||
*
|
||||
* Server component — no interactivity here; the CTA is an in-page anchor and the
|
||||
* feature visuals are static, tokenized, mono-for-data mock readouts.
|
||||
* DeskShowcase (DS5 · DESIGN-SPEC Part 6, #8) — the Desk-as-hero story above the
|
||||
* pricing grid. The pitch (left) is copy; the feature visuals (right) are now
|
||||
* REAL data (Truth-Everywhere Part 2, item 6), fetched from /api/desk-showcase:
|
||||
* - ALT LINE LADDER: a real graded prop's ladder (line + grade; edge only when
|
||||
* it's a plausible market value).
|
||||
* - PARLAY φ: the model's real same-team correlation, computed for two REAL
|
||||
* same-team legs (named).
|
||||
* The old hardcoded ladder / QUARTER-KELLY 2.4% / φ 0.34 mocks are gone. When
|
||||
* the board has no real ladder, the visuals HIDE (real-or-hidden) — the Kelly
|
||||
* tile is dropped entirely (the snapshot has no odds to size from honestly).
|
||||
*/
|
||||
|
||||
// A real alt-line-ladder rung (the Desk exclusive) — line + locked grade + edge.
|
||||
function Rung({ line, grade, edge, base }: { line: string; grade: string; edge: string; base?: boolean }) {
|
||||
const gradeCol = grade.startsWith('A') ? 'var(--g-a)' : grade.startsWith('B') ? 'var(--text-0)' : 'var(--amber)';
|
||||
const edgeCol = edge.startsWith('+') ? 'var(--g-a)' : edge.startsWith('-') ? 'var(--miss)' : 'var(--text-2)';
|
||||
interface Rung { line: number; grade: string; edge: number | null; base?: boolean }
|
||||
interface Showcase {
|
||||
available: boolean;
|
||||
ladder?: { player: string | null; stat_type: string | null; sport?: string; rungs: Rung[] } | null;
|
||||
parlay?: { value: number; legs: string[]; team?: string } | null;
|
||||
}
|
||||
|
||||
const STAT_LABEL: Record<string, string> = {
|
||||
total_bases: 'TB', home_runs: 'HR', hits: 'H', rbi: 'RBI', runs: 'R', doubles: '2B',
|
||||
strikeouts: 'K', points: 'PTS', rebounds: 'REB', assists: 'AST', threes: '3PT',
|
||||
};
|
||||
const statLabel = (s?: string | null) => (s ? (STAT_LABEL[s] || s.replace(/_/g, ' ').toUpperCase()) : '');
|
||||
|
||||
function RungCell({ line, grade, edge, base }: Rung) {
|
||||
const gradeCol = grade.startsWith('A') ? 'var(--g-a)' : grade.startsWith('B') ? 'var(--text-0)' : grade.startsWith('C') ? 'var(--text-1)' : 'var(--miss)';
|
||||
const edgeCol = edge == null ? 'var(--text-2)' : edge >= 0 ? 'var(--g-a)' : 'var(--miss)';
|
||||
return (
|
||||
<div style={{ flex: '1 1 0', minWidth: 58, textAlign: 'center', padding: '9px 6px', background: 'var(--bg-2)', border: `1px solid ${base ? 'var(--border-hi)' : 'var(--border)'}`, borderRadius: 6 }}>
|
||||
<div className="mono" style={{ fontSize: 11, color: base ? 'var(--text-0)' : 'var(--text-1)', marginBottom: 5 }}>{line}{base ? ' •' : ''}</div>
|
||||
<div className="mono" style={{ fontSize: 17, fontWeight: 800, color: gradeCol }}>{grade}</div>
|
||||
<div className="mono" style={{ fontSize: 10, color: edgeCol, marginTop: 3 }}>{edge}</div>
|
||||
<div className="mono" style={{ fontSize: 10, color: edgeCol, marginTop: 3 }}>
|
||||
{edge == null ? '—' : `${edge >= 0 ? '+' : ''}${edge}%`}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function DeskShowcase() {
|
||||
const [data, setData] = useState<Showcase | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let alive = true;
|
||||
fetch('/api/desk-showcase', { cache: 'no-store' })
|
||||
.then((r) => r.json())
|
||||
.then((d) => { if (alive) setData(d); })
|
||||
.catch(() => { if (alive) setData({ available: false }); });
|
||||
return () => { alive = false; };
|
||||
}, []);
|
||||
|
||||
const ladder = data?.available ? data.ladder : null;
|
||||
const parlay = data?.available ? data.parlay : null;
|
||||
const showVisuals = !!ladder && ladder.rungs.length > 0;
|
||||
|
||||
return (
|
||||
<section style={{ padding: '72px 24px 8px', borderTop: '1px solid var(--border)' }}>
|
||||
<div className="desk-showcase" style={{ maxWidth: 1100, margin: '0 auto', display: 'grid', gap: 40, alignItems: 'center' }}>
|
||||
@@ -56,46 +88,41 @@ export default function DeskShowcase() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* RIGHT — real feature visuals (no dead half) */}
|
||||
<div style={{ display: 'grid', gap: 14 }}>
|
||||
<div style={{ background: 'var(--bg-1)', border: '1px solid var(--border-hi)', borderRadius: 12, padding: 18 }}>
|
||||
<SectionHead style={{ marginBottom: 12 }}>
|
||||
ALT LINE LADDER <span style={{ color: 'var(--amber)', fontSize: 9, border: '1px solid rgba(255,179,71,.4)', borderRadius: 3, padding: '1px 5px', marginLeft: 4 }}>DESK</span>
|
||||
</SectionHead>
|
||||
<div style={{ display: 'flex', gap: 7 }}>
|
||||
<Rung line="1.5" grade="A" edge="+7.1%" />
|
||||
<Rung line="2.5" grade="A+" edge="+11.4%" base />
|
||||
<Rung line="3.5" grade="B" edge="+2.0%" />
|
||||
<Rung line="4.5" grade="C" edge="-3.8%" />
|
||||
{/* RIGHT — REAL feature visuals (hidden when the board has none) */}
|
||||
{showVisuals && (
|
||||
<div style={{ display: 'grid', gap: 14 }}>
|
||||
<div style={{ background: 'var(--bg-1)', border: '1px solid var(--border-hi)', borderRadius: 12, padding: 18 }}>
|
||||
<SectionHead style={{ marginBottom: 4 }}>
|
||||
ALT LINE LADDER <span style={{ color: 'var(--amber)', fontSize: 9, border: '1px solid rgba(255,179,71,.4)', borderRadius: 3, padding: '1px 5px', marginLeft: 4 }}>DESK</span>
|
||||
</SectionHead>
|
||||
<div className="mono" style={{ fontSize: 10.5, color: 'var(--text-2)', marginBottom: 10, letterSpacing: '0.04em' }}>
|
||||
{ladder!.player} · {statLabel(ladder!.stat_type)}
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 7, flexWrap: 'wrap' }}>
|
||||
{ladder!.rungs.map((r) => <RungCell key={r.line} {...r} />)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gap: 14, gridTemplateColumns: '1fr 1fr' }}>
|
||||
<div style={{ background: 'var(--bg-1)', border: '1px solid var(--border)', borderRadius: 12, padding: 16 }}>
|
||||
<SectionHead style={{ marginBottom: 10 }}>QUARTER-KELLY</SectionHead>
|
||||
<div className="mono" style={{ fontSize: 26, fontWeight: 800, color: 'var(--g-a)', fontVariantNumeric: 'tabular-nums' }}>2.4%</div>
|
||||
<div className="mono" style={{ fontSize: 11, color: 'var(--text-tertiary)', marginTop: 4 }}>of bankroll · at -110</div>
|
||||
</div>
|
||||
<div style={{ background: 'var(--bg-1)', border: '1px solid var(--border)', borderRadius: 12, padding: 16 }}>
|
||||
<SectionHead style={{ marginBottom: 10 }}>PARLAY φ</SectionHead>
|
||||
<div className="mono" style={{ fontSize: 26, fontWeight: 800, color: 'var(--text-0)', fontVariantNumeric: 'tabular-nums' }}>0.34</div>
|
||||
<div className="mono" style={{ fontSize: 11, color: 'var(--text-tertiary)', marginTop: 4 }}>correlation · same-team legs</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* PARLAY φ — real, only when two real same-team legs exist */}
|
||||
{parlay && (
|
||||
<div style={{ background: 'var(--bg-1)', border: '1px solid var(--border)', borderRadius: 12, padding: 16 }}>
|
||||
<SectionHead style={{ marginBottom: 10 }}>PARLAY φ</SectionHead>
|
||||
<div className="mono" style={{ fontSize: 26, fontWeight: 800, color: 'var(--text-0)', fontVariantNumeric: 'tabular-nums' }}>{parlay.value}</div>
|
||||
<div className="mono" style={{ fontSize: 11, color: 'var(--text-tertiary)', marginTop: 4 }}>
|
||||
correlation · {parlay.legs[0]} + {parlay.legs[1]}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Wave 4A (Step 4) — this claim is now BACKED by a real feature: the
|
||||
CONSENSUS vs MODEL strip (components/vyndr/MarketBreadth, fed by
|
||||
lib/marketBreadth.collectBreadth) ships on the live slate/dashboard,
|
||||
computing the median book line vs the model's projection. "live
|
||||
line moves" is the existing snapshot line-deltas / LineSparkline.
|
||||
No longer an empty promise — do not remove without removing those. */}
|
||||
<div style={{ background: 'var(--bg-1)', border: '1px solid var(--border)', borderRadius: 12, padding: '14px 16px', display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
<span className="live-dot" style={{ background: 'var(--g-a)' }} aria-hidden />
|
||||
<span className="mono" style={{ fontSize: 11.5, color: 'var(--text-1)', letterSpacing: '0.04em' }}>
|
||||
REAL-TIME FEED · <span style={{ color: 'var(--g-a)', fontWeight: 700 }}>consensus vs model</span>, live line moves
|
||||
</span>
|
||||
{/* Real-time feed — backed by the live MarketBreadth (consensus vs model). */}
|
||||
<div style={{ background: 'var(--bg-1)', border: '1px solid var(--border)', borderRadius: 12, padding: '14px 16px', display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
<span className="live-dot" style={{ background: 'var(--g-a)' }} aria-hidden />
|
||||
<span className="mono" style={{ fontSize: 11.5, color: 'var(--text-1)', letterSpacing: '0.04em' }}>
|
||||
REAL-TIME FEED · <span style={{ color: 'var(--g-a)', fontWeight: 700 }}>consensus vs model</span>, live line moves
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user