{
+ const src = read('components/vyndr/ClaimMeter.tsx');
+ it('renders the amber founder-seat scarcity bar', () => {
+ expect(src).toContain('CLAIMED');
+ expect(src).toContain('var(--amber)');
+ });
+});
+
+describe('Phase D — Terminal page (real intelligence, not a stub)', () => {
+ const src = read('app/terminal/page.tsx');
+ it('is no longer a RouteStub', () => {
+ expect(src).not.toContain('RouteStub');
+ });
+ it('uses the intel-surface and renders the VVI / cascade / leaders sections', () => {
+ expect(src).toContain('intel-surface');
+ expect(src).toContain('VOLATILITY INDEX');
+ expect(src).toContain('INJURY WIRE');
+ expect(src).toContain('GRADEABLE LEADERS');
+ });
+});
+
+describe('Phase D — scan + landing wiring', () => {
+ it('scan renders the new ProcessingGrade card via the adapter (legacy GradeCard removed)', () => {
+ const src = read('app/scan/page.tsx');
+ expect(src).toContain('ProcessingGrade');
+ expect(src).toContain('mapScanToGradeResult');
+ expect(src).not.toContain("from '@/components/GradeCard'");
+ });
+ it('scan keeps sportsbook deep-links safe (noopener noreferrer)', () => {
+ expect(read('app/scan/page.tsx')).toContain('noopener noreferrer');
+ });
+ it('landing mounts the ClaimMeter', () => {
+ expect(read('app/page.tsx')).toContain('ClaimMeter');
+ });
+});
diff --git a/web/src/app/page.tsx b/web/src/app/page.tsx
index d8f5823..6edae41 100644
--- a/web/src/app/page.tsx
+++ b/web/src/app/page.tsx
@@ -4,6 +4,7 @@ import { useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { useAuth } from '@/contexts/AuthContext';
import Hero from '@/components/Hero';
+import { ClaimMeter } from '@/components/vyndr';
// Session 17 — game-count strip mounted between the hero and the
// existing LivePropsStrip. Shows "X NBA · Y WNBA · Z MLB games
// being graded right now" with a signup CTA. Hides itself when
@@ -44,6 +45,10 @@ export default function Home() {
return (
<>
+ {/* Founder-seat scarcity meter (§12) */}
+
+
+
diff --git a/web/src/app/scan/page.tsx b/web/src/app/scan/page.tsx
index d671c13..6ff7145 100644
--- a/web/src/app/scan/page.tsx
+++ b/web/src/app/scan/page.tsx
@@ -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 = {
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() {
)}
- {/* 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 && (
-
{
- trackUpgradeClicked({ current_tier: tier, target_tier: target, trigger_location: from });
- router.push(`/api/checkout?tier=${target}`);
- }}
+ {
addLeg({
sport,
@@ -680,8 +702,39 @@ export default function ScanPage() {
});
open();
}}
+ onReadAnother={reset}
/>
+ {/* Sportsbook hand-off (preserved feature) */}
+
+
+ {/* Free-tier nudge — full paywall treatment returns in Phase G */}
+ {tier === 'free' && (
+
+ )}
+