Session 36: Design system Phase E — remaining screens + dashboard Bloomberg lines (1853 tests)

VYNDR 2.0 conversion, Phase E. Frontend-only; zero backend changes.

- lib/slateAdapter.js: parseAmericanOdds, detectBestLines, mapScheduleToGameCards
  (best/worst line detection — the Bloomberg pattern).
- Reskinned the LEGACY GameCard's game-lines grid with best/worst highlighting +
  SportBadge, keeping inline grading intact (a wholesale swap to the display-only
  vyndr/GameCard would have deleted the slate's grading interaction).
- compare/invite/help/about: RouteStubs -> real design-system pages.
- login reskinned (scanlines, system voice, new Wordmark); pricing + ClaimMeter.

Honest scope: the full GameCard swap needs inline grading ported into the new
component first; profile/settings/blog/game-detail reskins are light/deferred.

18 new tests. Backend 1839 -> 1853, 144 suites, zero regressions. Web build clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kev
2026-06-16 01:04:37 -04:00
parent 1d83682cdb
commit 612f5e0b72
12 changed files with 599 additions and 96 deletions
+57 -7
View File
@@ -1,13 +1,63 @@
import RouteStub from '@/components/vyndr/RouteStub';
'use client';
export const metadata = { title: 'Invite' };
import { useState } from 'react';
import SectionHead from '@/components/vyndr/SectionHead';
import VBtn from '@/components/vyndr/VBtn';
import { useAuth } from '@/contexts/AuthContext';
// Referral system (§12) — bring 3 friends → free Analyst. Progress is read
// from the session when available; the live invite ledger wires in later.
export default function InvitePage() {
const { user } = useAuth();
const [copied, setCopied] = useState(false);
const code = (user?.email?.split('@')[0] || 'signal').replace(/[^a-z0-9]/gi, '').toUpperCase().slice(0, 10) || 'SIGNAL';
const link = `https://vyndr.app/?ref=${code}`;
const invited = 0;
const goal = 3;
const copy = () => {
if (typeof navigator !== 'undefined' && navigator.clipboard) {
void navigator.clipboard.writeText(link);
setCopied(true);
setTimeout(() => setCopied(false), 1800);
}
};
return (
<RouteStub
title="Invite"
arriving="SESSION 36"
blurb="Bring three friends onto the signal and your Analyst plan is on the house."
/>
<section style={{ maxWidth: 640, margin: '0 auto', padding: '28px 16px 96px' }}>
<SectionHead accent="var(--amber)"> REFERRAL</SectionHead>
<h1 className="mono" style={{ fontSize: 28, fontWeight: 800, letterSpacing: '-0.02em', margin: '8px 0 8px' }}>INVITE</h1>
<p className="mono" style={{ fontSize: 14, color: 'var(--text-1)', lineHeight: 1.6, marginBottom: 24 }}>
Bring <span style={{ color: 'var(--g-a)', fontWeight: 700 }}>3 friends</span> onto the signal and your Analyst plan is on the house locked for as long as they stay.
</p>
{/* Progress */}
<div style={{ marginBottom: 24 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 8 }}>
<span className="label">FRIENDS JOINED</span>
<span className="mono" style={{ fontSize: 13, fontWeight: 700, color: 'var(--g-a)' }}>{invited} / {goal}</span>
</div>
<div style={{ display: 'flex', gap: 6 }}>
{Array.from({ length: goal }).map((_, i) => (
<div key={i} style={{ flex: 1, height: 6, borderRadius: 4, background: i < invited ? 'var(--g-a)' : 'var(--bg-2)', border: '1px solid var(--border)', boxShadow: i < invited ? '0 0 10px rgba(0,212,160,.5)' : 'none' }} />
))}
</div>
</div>
{/* Referral link */}
<div style={{ marginBottom: 16 }}>
<span className="label">YOUR LINK</span>
<div style={{ display: 'flex', gap: 8, marginTop: 8 }}>
<div className="mono" style={{ flex: 1, padding: '11px 14px', background: 'var(--bg-2)', border: '1px solid var(--border)', borderRadius: 6, fontSize: 13, color: 'var(--g-a)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{link}
</div>
<VBtn variant={copied ? 'primary' : 'outline'} onClick={copy}>{copied ? '✓ Copied' : 'Copy'}</VBtn>
</div>
</div>
<div style={{ display: 'flex', gap: 10 }}>
<VBtn variant="primary" style={{ flex: 1 }} onClick={copy}> Share your link</VBtn>
</div>
</section>
);
}