Merge S3 (a1): affiliate + partner plumbing

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kev
2026-07-11 14:31:53 -04:00
23 changed files with 1151 additions and 36 deletions
+4
View File
@@ -25,6 +25,10 @@ export interface PropRowProp {
line: number;
direction: PropDirection;
book?: string;
// A1 S3 — the FULL per-book rows for this player+stat (the grouped
// odds shape from Express). Feeds best-price detection downstream;
// optional so legacy flat callers stay valid.
books?: Array<{ book?: string; line?: number; over_odds?: number | null; under_odds?: number | null }>;
// Stable key used by the parent to look up grade results.
key?: string;
}
+3
View File
@@ -345,6 +345,9 @@ function groupByGame(rawProps: RawProp[], sport: SlateSport): SlateGame[] {
line: lineInfo.line,
direction: lineInfo.direction,
book: lineInfo.book,
// A1 S3 — keep the full per-book rows so the strip layer can mark
// the best available price (pickLine alone discards the comparison).
books: Array.isArray(r.lines) ? r.lines : undefined,
});
}
// Sort each game's props by player + stat for stable rendering.
@@ -0,0 +1,16 @@
'use client';
import { useEffect } from 'react';
import { captureRef } from '@/lib/partnerRef';
/**
* PartnerRefCapture (A1 S3) — mounted once in the layout (GlobalHosts
* pattern). On first visit with ?ref=CODE it sets the first-party
* `vyndr_ref` cookie (90 days, first-touch). Renders nothing.
*/
export default function PartnerRefCapture() {
useEffect(() => {
captureRef(window);
}, []);
return null;
}
+45 -6
View File
@@ -1,6 +1,10 @@
import ArchetypeBadge from '@/components/vyndr/ArchetypeBadge';
import GradeBadge from '@/components/vyndr/GradeBadge';
import { nextRunLabelET } from '@/lib/pipelineSchedule';
// A1 S3 — BOOK IT is a real per-book deep link now (organic until the
// affiliate config flips a book on). rel MUST stay BOOK_LINK_REL.
import { buildBookLink, BOOK_LINK_REL } from '@/lib/bookLinks';
import { bookInfo } from '@/lib/books';
export interface StatCell {
label: string;
@@ -20,6 +24,10 @@ export interface StripProp {
// Session 60 (Phase 2.5) — intraday movement + public revision.
movement?: { kind: 'steam' | 'value' | 'against' | 'revised' | string; delta: number; currentLine: number } | null;
revisedFrom?: string | null;
// A1 S3 — the prop's source book + the best available price across books
// (only set when ≥2 books post the same line and prices differ).
book?: string | null;
bestBook?: { book: string; odds: number } | null;
}
/** Phase 2.5 movement chip: STEAM ▲ (market chasing), VALUE ▲ (better
@@ -75,6 +83,7 @@ const Sep = ({ ch = '|' }: { ch?: string }) => (
export default function StatStrip({
player,
team,
sport,
archetype,
stats,
last10,
@@ -125,17 +134,45 @@ export default function StatStrip({
</span>
);
};
// Session 52 — Push-to-Book teaser on graded props (feature not live yet).
// A1 S3 — BOOK IT is a real sportsbook deep link (was the Session 52
// teaser). Targets the best-priced book when one is known, else the
// prop's source book, else DraftKings. Links are ORGANIC until the
// operator flips a book in affiliateConfig — the anchor is identical
// either way, and rel is always BOOK_LINK_REL (sponsored noopener
// noreferrer) for affiliate compliance.
const BookItTeaser = ({ p }: { p: StripProp }) => {
if (!p.grade) return null;
const target = (p.bestBook && p.bestBook.book) || p.book || 'draftkings';
const link = buildBookLink({ book: target, player, sport });
if (!link) return null;
return (
<span
<a
className="mono"
title="Push-to-Book coming soon — connect your sportsbook"
style={{ fontSize: 9, fontWeight: 700, letterSpacing: '0.06em', color: 'var(--text-2)', cursor: 'default', padding: '2px 5px', borderRadius: 4, border: '1px solid var(--border)', opacity: 0.6 }}
href={link.url}
target="_blank"
rel={BOOK_LINK_REL}
onClick={(e) => e.stopPropagation()}
title={`Open ${bookInfo(target).name} — search lands on ${player}`}
style={{ fontSize: 9, fontWeight: 700, letterSpacing: '0.06em', color: 'var(--text-1)', cursor: 'pointer', padding: '2px 5px', borderRadius: 4, border: '1px solid var(--border)', textDecoration: 'none' }}
>
BOOK IT
</span>
</a>
);
};
// A1 S3 — best available price marker. ONE meaning: this prop's line is
// posted at a better price at `bestBook` than at least one other book.
// Renders only when the adapter verified ≥2 books at the same line.
const BestPriceDot = ({ p }: { p: StripProp }) => {
if (!p.bestBook) return null;
const odds = p.bestBook.odds;
const oddsStr = typeof odds === 'number' && odds > 0 ? `+${odds}` : String(odds);
return (
<span
title={`Best available price · ${bookInfo(p.bestBook.book).name} ${oddsStr}`}
aria-label={`Best available price at ${bookInfo(p.bestBook.book).name}, ${oddsStr}`}
style={{ width: 6, height: 6, borderRadius: '50%', display: 'inline-block', flexShrink: 0,
background: 'var(--g-a)', boxShadow: '0 0 0 2px rgba(0,212,160,.22)' }}
/>
);
};
const last10Str = typeof last10 === 'string'
@@ -226,7 +263,7 @@ export default function StatStrip({
<span key={i} style={{ display: 'inline-flex', alignItems: 'center', gap: 10 }}>
{i > 0 && <span style={{ color: '#3A3A48' }}>·</span>}
<span className="mono" style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 11, color: '#B8BCC8' }}>
{p.stat} {p.side}{p.line} {p.grade && <GradeBadge grade={p.grade} size="sm" />}
{p.stat} {p.side}{p.line} <BestPriceDot p={p} /> {p.grade && <GradeBadge grade={p.grade} size="sm" />}
<ParlayBtn p={p} />
<BookItTeaser p={p} />
</span>
@@ -245,6 +282,7 @@ export default function StatStrip({
return (
<div key={i} className="mono" style={{ fontSize: 11, color: 'var(--text-2)', display: 'flex', alignItems: 'center', gap: 8 }}>
<span style={{ color: 'var(--text-1)' }}>{p.stat} {p.line}</span>
<BestPriceDot p={p} />
<span style={{ color: 'var(--text-2)', fontStyle: 'italic' }}>{next ? `Grades post ${next}` : 'Awaiting next scan'}</span>
</div>
);
@@ -254,6 +292,7 @@ export default function StatStrip({
<div key={i} style={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<div className="mono" style={{ display: 'inline-flex', alignItems: 'center', gap: 7, fontSize: 12, color: '#B8BCC8' }}>
<span style={{ color: '#fff' }}>{p.stat} {p.side}{p.line}</span>
<BestPriceDot p={p} />
{/* Phase 2.5 — a revised grade is PUBLIC: original struck through. */}
{p.revisedFrom && (
<span className="mono" title="Grade revised after the line moved against the read — original preserved" style={{ fontSize: 10.5, color: 'var(--text-2)', textDecoration: 'line-through' }}>{p.revisedFrom}</span>