-
- {player} · {stat.replace(/_/g, ' ')}{line != null ? ` ${line}` : ''} · {side}
-
+ {showHeader && (
+
+ {player} · {stat.replace(/_/g, ' ')} · {side}
+
+ )}
{books.map((b) => (
-
+
+ {b.line != null ? `${sideChar}${b.line}` : '—'}
+
+
{fmt(b.over_odds)} / {fmt(b.under_odds)}
{b.isBest ? (
@@ -67,6 +79,15 @@ export default function BookComparison({ player, stat, line, side = 'over', book
))}
+ {/* Honest single-book state: intentional, not broken — no crown, no implied
+ second row, no "comparison" framing over a lone price. */}
+ {single && (
+
Betting the best line saves ~${savings.toFixed(2)} per $100.
diff --git a/web/src/components/vyndr/BookComparisonPanel.tsx b/web/src/components/vyndr/BookComparisonPanel.tsx
new file mode 100644
index 0000000..cf8c995
--- /dev/null
+++ b/web/src/components/vyndr/BookComparisonPanel.tsx
@@ -0,0 +1,70 @@
+'use client';
+
+import { useEffect, useState } from 'react';
+import BookComparison, { type BookRow } from '@/components/BookComparison';
+import SectionHead from '@/components/vyndr/SectionHead';
+
+/**
+ * BookComparisonPanel — the ONE place the (previously-dead) BookComparison grid
+ * is wired to a real feed. Self-fetches `/api/books/:sport/:player/:stat` (which
+ * reads the snapshot-locked, display-only `bookprices` store — fenced from the
+ * grade path) and renders BookComparison's honest states:
+ * - single-book (the common case): one book, no crown, no fake second row
+ * - multi-book: every book's OWN line + price, none crowned (crown is gated OFF)
+ * - no books / error: honest-absent — renders NOTHING (the card omits the section)
+ *
+ * DISPLAY-ONLY. It reads a public read-only endpoint; it touches no grade, line,
+ * projection, or price on the card. Freshness: `bookprices` is written in the SAME
+ * snapshot that locks the grade (same `props`, same `ts`), so these prices are as
+ * fresh as the graded line — no drift, no stale-label needed.
+ */
+interface Resp {
+ books?: BookRow[];
+ line?: number | null;
+ savings?: number;
+}
+
+export default function BookComparisonPanel({
+ sport,
+ player,
+ stat,
+ side,
+}: {
+ sport: string;
+ player: string;
+ stat: string;
+ side: 'Over' | 'Under';
+}) {
+ const [data, setData] = useState
(null);
+ const [done, setDone] = useState(false);
+
+ useEffect(() => {
+ let alive = true;
+ if (!sport || !player || !stat) { setDone(true); return; }
+ const s = side === 'Under' ? 'under' : 'over';
+ fetch(`/api/books/${encodeURIComponent(sport)}/${encodeURIComponent(player)}/${encodeURIComponent(stat)}?side=${s}`, { cache: 'no-store' })
+ .then((r) => (r.ok ? r.json() : null))
+ .then((d: Resp | null) => { if (alive) { setData(d && Array.isArray(d.books) ? d : null); setDone(true); } })
+ .catch(() => { if (alive) setDone(true); });
+ return () => { alive = false; };
+ }, [sport, player, stat, side]);
+
+ // Honest-absent: until we have real book rows, render nothing at all — never a
+ // placeholder, never a fabricated book, never an empty labelled box.
+ if (!done || !data || !Array.isArray(data.books) || data.books.length === 0) return null;
+
+ return (
+
+ BOOK COMPARISON
+
+
+ );
+}
diff --git a/web/src/components/vyndr/GradeResultCard.tsx b/web/src/components/vyndr/GradeResultCard.tsx
index dc90aae..6c18adb 100644
--- a/web/src/components/vyndr/GradeResultCard.tsx
+++ b/web/src/components/vyndr/GradeResultCard.tsx
@@ -9,6 +9,7 @@ import ArchetypeBlend from '@/components/vyndr/ArchetypeBlend';
import GradeBadge from '@/components/vyndr/GradeBadge';
import GradeShift from '@/components/vyndr/GradeShift';
import PlayerAvatar from '@/components/vyndr/PlayerAvatar';
+import BookComparisonPanel from '@/components/vyndr/BookComparisonPanel';
import { type HeadshotSport } from '@/lib/playerHeadshot';
import { gradeColor, gradeHex } from '@/lib/vyndrTokens';
import { edgeColor, gradeGlows } from '@/lib/colorContract';
@@ -93,7 +94,6 @@ export default function GradeResultCard({
// colorContract.edgeColor helper (imported) — negative = var(--miss). DS4
// introduced a local const that DS3 superseded with the shared enforcer.
const hasKill = !!d.killConditions && d.killConditions.length > 0;
- const hasBooks = Array.isArray(d.books) && d.books.length > 0;
const hasAlt = Array.isArray(d.altLadder) && d.altLadder.length > 0;
return (
@@ -303,24 +303,12 @@ export default function GradeResultCard({
)}
- {/* 7. BOOK COMPARISON */}
- {hasBooks && (
-