S3 (a1): affiliate + partner plumbing
Zero out-of-pocket; everything config-flip-ready but DISABLED/organic. - BOOK IT deep links: web/src/lib/bookLinks.js + affiliateConfig.js (all books enabled:false, Impact/Partnerize param shapes documented, empty params skipped). Wired into StatStrip BookItTeaser (real anchor now) + scan hand-off links. Every book anchor renders rel="sponsored noopener noreferrer" (BOOK_LINK_REL). - Best-price marker: slateAdapter.detectBestBook (only when >=2 books post the SAME line and prices differ — absent beats wrong) + subtle signal-green dot in StatStrip. Slate.groupByGame threads the grouped per-book rows (books[]) onto PropRowProp instead of discarding them. - Partner refs: ?ref=CODE -> vyndr_ref cookie (90d, first-touch, PartnerRefCapture in layout) -> signup metadata partner_ref -> internal GET /api/partners/report/:code (requireInternalAuth; honest zeros + note until the TODO migration in docs/PARTNERS.md adds user_profiles.partner_ref — NOT run). Stripe promo-code convention: partner code == promotion code, verbatim. - Tests: +41 (2398 -> 2439, 209 suites); bookItTeaser + vyndrCoreScreens invariants updated to the new (stronger) rel contract. Web build exit 0. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,7 @@ import Footer from '@/components/Footer';
|
||||
import AuthGate from '@/components/AuthGate';
|
||||
import HashRedirect from '@/components/vyndr/HashRedirect';
|
||||
import GlobalHosts from '@/components/vyndr/GlobalHosts';
|
||||
import PartnerRefCapture from '@/components/vyndr/PartnerRefCapture';
|
||||
// Session 50 — the Parlay Lab supersedes the legacy ParlayTray.
|
||||
import ParlayPanel from '@/components/vyndr/ParlayPanel';
|
||||
import BottomTabBar from '@/components/BottomTabBar';
|
||||
@@ -156,6 +157,8 @@ export default async function RootLayout({ children }: { children: React.ReactNo
|
||||
<SentryInit />
|
||||
{/* Session 38 — prefs apply + paywall/checkout/prefs globals (§9/§10/§12) */}
|
||||
<GlobalHosts />
|
||||
{/* A1 S3 — first-visit ?ref=CODE → vyndr_ref cookie (90d, first-touch) */}
|
||||
<PartnerRefCapture />
|
||||
</ParlayProvider>
|
||||
</ExplainModeProvider>
|
||||
</AuthProvider>
|
||||
|
||||
+19
-21
@@ -17,6 +17,7 @@ import {
|
||||
trackUpgradeClicked,
|
||||
} from '@/lib/analytics';
|
||||
import { getHeadshotUrl, PLAYER_SILHOUETTE, type HeadshotSport } from '@/lib/playerHeadshot';
|
||||
import { buildBookLink, SUPPORTED_BOOKS, BOOK_LINK_REL } from '@/lib/bookLinks';
|
||||
|
||||
type Sport = 'NBA' | 'MLB' | 'WNBA';
|
||||
|
||||
@@ -103,15 +104,8 @@ 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)}`;
|
||||
// Sportsbook deep-links — A1 S3: built by lib/bookLinks (organic until the
|
||||
// affiliate config flips a book on). rel is BOOK_LINK_REL on every anchor.
|
||||
|
||||
export default function ScanPage() {
|
||||
const router = useRouter();
|
||||
@@ -775,18 +769,22 @@ export default function ScanPage() {
|
||||
|
||||
{/* 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>
|
||||
))}
|
||||
{SUPPORTED_BOOKS.map((b) => {
|
||||
const link = buildBookLink({ book: b.id, player: selectedPlayer, sport });
|
||||
if (!link) return null;
|
||||
return (
|
||||
<a
|
||||
key={b.id}
|
||||
href={link.url}
|
||||
target="_blank"
|
||||
rel={BOOK_LINK_REL}
|
||||
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 */}
|
||||
|
||||
Reference in New Issue
Block a user