Session 35: Design system Phase D — core screens: Grade Result, Slate card, Scan, Terminal, Landing (1839 tests)
VYNDR 2.0 conversion, Phase D (the screens users touch). Frontend-only; zero backend changes. - GradeResultCard + ProcessingGrade (the core product moment): intel-surface grade hero, signal breakdown, kill conditions, best-book strip, alt ladder; sections self-hide when empty. - lib/gradeAdapter.js maps engine output -> §7 contract and tier-gates content (free teaser / analyst kill-conditions / desk alt ladder) so the new card doesn't give paid content away. - Scan result wired to ProcessingGrade->GradeResultCard, preserving scan limits, parlay add, reads tracking, and noopener sportsbook deep-links. - GameCard (Bloomberg best/worst line cells) built + tested. - Terminal page replaces its stub with a real league-intelligence screen. - Landing gets the founder-seat ClaimMeter. Honest scope: live dashboard/Slate swap onto GameCard, scan input -> TerminalInput, full landing rebuild, and the blurred-paywall polish (Phase G) are deferred to keep working flows stable. 22 new tests. Backend 1818 -> 1839, 143 suites, zero regressions. Web build clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+75
-22
@@ -2,7 +2,10 @@
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import GradeCard from '@/components/GradeCard';
|
||||
import ProcessingGrade from '@/components/vyndr/ProcessingGrade';
|
||||
import type { GradeResultData } from '@/components/vyndr/GradeResultCard';
|
||||
import { mapScanToGradeResult } from '@/lib/gradeAdapter';
|
||||
import { markReadComplete } from '@/lib/reads';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { useParlay } from '@/contexts/ParlayContext';
|
||||
import {
|
||||
@@ -83,6 +86,16 @@ const SPORT_ACCENT: Record<Sport, string> = {
|
||||
WNBA: '#FFB347',
|
||||
};
|
||||
|
||||
// Sportsbook deep-links — preserved from the legacy GradeCard so the new
|
||||
// design keeps the book hand-off. target=_blank + noopener,noreferrer.
|
||||
const SPORTSBOOKS = [
|
||||
{ id: 'draftkings', label: 'DraftKings', host: 'sportsbook.draftkings.com' },
|
||||
{ id: 'fanduel', label: 'FanDuel', host: 'sportsbook.fanduel.com' },
|
||||
{ id: 'betmgm', label: 'BetMGM', host: 'sports.betmgm.com' },
|
||||
{ id: 'caesars', label: 'Caesars', host: 'sportsbook.caesars.com' },
|
||||
];
|
||||
const deepLink = (host: string, player: string) => `https://${host}/?search=${encodeURIComponent(player)}`;
|
||||
|
||||
export default function ScanPage() {
|
||||
const router = useRouter();
|
||||
const { user, tier, scansRemaining, canScan, loading: authLoading, bumpScanCount } = useAuth();
|
||||
@@ -249,6 +262,17 @@ export default function ScanPage() {
|
||||
}
|
||||
};
|
||||
|
||||
// Count a completed read once per prop per session (drives the Install/Push
|
||||
// prompt gates) — preserved from the legacy GradeCard's reveal effect.
|
||||
useEffect(() => {
|
||||
if (!result || typeof window === 'undefined') return;
|
||||
const readKey = `vyndr_read_${sport}_${selectedPlayer}_${stat}_${line}_${direction}`;
|
||||
if (!window.sessionStorage.getItem(readKey)) {
|
||||
window.sessionStorage.setItem(readKey, '1');
|
||||
markReadComplete();
|
||||
}
|
||||
}, [result, sport, selectedPlayer, stat, line, direction]);
|
||||
|
||||
const reset = () => {
|
||||
setResult(null);
|
||||
setError('');
|
||||
@@ -645,29 +669,27 @@ export default function ScanPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Grade card output */}
|
||||
{/* Grade result — VYNDR 2.0 ProcessingGrade → GradeResultCard (Session 35).
|
||||
Engine output is mapped to the §7 contract and tier-gated by the adapter. */}
|
||||
{result && (
|
||||
<div style={{ marginTop: 32, display: 'grid', gap: 16 }}>
|
||||
<GradeCard
|
||||
sport={sport}
|
||||
player={selectedPlayer}
|
||||
stat={stat}
|
||||
line={Number(line)}
|
||||
direction={direction}
|
||||
grade={result.grade}
|
||||
projection={result.projection}
|
||||
confidence={result.confidence}
|
||||
sample_size={result.sample_size}
|
||||
factors={result.factors}
|
||||
alt_lines={result.alt_lines}
|
||||
kill_conditions={result.kill_conditions}
|
||||
reasoning={result.reasoning}
|
||||
historical_hit_rate={result.historical_hit_rate}
|
||||
tier={tier}
|
||||
onUpgradeClick={(target, from) => {
|
||||
trackUpgradeClicked({ current_tier: tier, target_tier: target, trigger_location: from });
|
||||
router.push(`/api/checkout?tier=${target}`);
|
||||
}}
|
||||
<ProcessingGrade
|
||||
key={`${selectedPlayer}-${stat}-${line}-${direction}`}
|
||||
data={mapScanToGradeResult({
|
||||
player: selectedPlayer,
|
||||
sport,
|
||||
stat,
|
||||
line: Number(line),
|
||||
direction,
|
||||
grade: result.grade,
|
||||
projection: result.projection,
|
||||
confidence: result.confidence,
|
||||
sample_size: result.sample_size,
|
||||
factors: result.factors,
|
||||
alt_lines: result.alt_lines,
|
||||
kill_conditions: result.kill_conditions,
|
||||
tier,
|
||||
}) as GradeResultData}
|
||||
onAddToParlay={() => {
|
||||
addLeg({
|
||||
sport,
|
||||
@@ -680,8 +702,39 @@ export default function ScanPage() {
|
||||
});
|
||||
open();
|
||||
}}
|
||||
onReadAnother={reset}
|
||||
/>
|
||||
|
||||
{/* Sportsbook hand-off (preserved feature) */}
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8, justifyContent: 'center' }}>
|
||||
{SPORTSBOOKS.map((b) => (
|
||||
<a
|
||||
key={b.id}
|
||||
href={deepLink(b.host, selectedPlayer)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="mono"
|
||||
style={{ padding: '7px 13px', fontSize: 11, fontWeight: 700, borderRadius: 6, border: '1px solid var(--border-hi)', color: 'var(--text-1)', textDecoration: 'none' }}
|
||||
>
|
||||
{b.label} ↗
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Free-tier nudge — full paywall treatment returns in Phase G */}
|
||||
{tier === 'free' && (
|
||||
<button
|
||||
onClick={() => {
|
||||
trackUpgradeClicked({ current_tier: tier, target_tier: 'analyst', trigger_location: 'grade_card_teaser' });
|
||||
router.push('/api/checkout?tier=analyst');
|
||||
}}
|
||||
className="mono"
|
||||
style={{ padding: '12px 16px', borderRadius: 8, border: '1px solid rgba(255,179,71,.4)', background: 'rgba(255,179,71,.06)', color: 'var(--amber)', fontWeight: 700, fontSize: 13, cursor: 'pointer' }}
|
||||
>
|
||||
Unlock every signal + kill conditions — $14.99/mo
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div style={{ display: 'flex', gap: 12 }}>
|
||||
<button onClick={reset} className="btn-ghost" style={{ flex: 1 }}>
|
||||
Read another prop
|
||||
|
||||
Reference in New Issue
Block a user