|
|
|
@@ -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>
|
|
|
|
|