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:
@@ -1,13 +1,64 @@
|
||||
import RouteStub from '@/components/vyndr/RouteStub';
|
||||
'use client';
|
||||
|
||||
export const metadata = { title: 'Help' };
|
||||
import { useMemo, useState } from 'react';
|
||||
import SectionHead from '@/components/vyndr/SectionHead';
|
||||
import TerminalInput from '@/components/vyndr/TerminalInput';
|
||||
|
||||
const FAQ: { cat: string; q: string; a: string }[] = [
|
||||
{ cat: 'GRADES', q: 'What does a VYNDR grade mean?', a: 'A grade is our confidence that a prop hits, weighed across 40+ factors. A+ is the rarest and strongest; D means stay away. The letter is earned, not opinion.' },
|
||||
{ cat: 'GRADES', q: 'What is a kill condition?', a: 'A red flag we detect that can sink a prop regardless of the stats — a blowout risk, a minutes cap, a brutal matchup. Analyst and Desk see them in full.' },
|
||||
{ cat: 'READS', q: 'How many free reads do I get?', a: 'Five reads every calendar month on the Free tier. The counter resets at the start of each month.' },
|
||||
{ cat: 'READS', q: 'What counts as a read?', a: 'Grading one prop. Viewing the same prop twice in a session only counts once.' },
|
||||
{ cat: 'PLANS', q: 'What do Analyst and Desk unlock?', a: 'Analyst unlocks unlimited reads, every signal, and kill conditions. Desk adds the alt-line ladder and the full intelligence layer.' },
|
||||
{ cat: 'DATA', q: 'Where does the data come from?', a: 'Live odds, schedules, injuries, and box scores from multiple providers — refreshed continuously so the signal stays current.' },
|
||||
];
|
||||
|
||||
export default function HelpPage() {
|
||||
const [query, setQuery] = useState('');
|
||||
const [open, setOpen] = useState<number | null>(null);
|
||||
const filtered = useMemo(() => {
|
||||
const q = query.trim().toLowerCase();
|
||||
if (!q) return FAQ;
|
||||
return FAQ.filter((f) => f.q.toLowerCase().includes(q) || f.a.toLowerCase().includes(q) || f.cat.toLowerCase().includes(q));
|
||||
}, [query]);
|
||||
|
||||
return (
|
||||
<RouteStub
|
||||
title="Help & FAQ"
|
||||
arriving="SESSION 36"
|
||||
blurb="Searchable answers on grades, reads, plans, and how to read the signal."
|
||||
/>
|
||||
<section style={{ maxWidth: 720, margin: '0 auto', padding: '28px 16px 96px' }}>
|
||||
<SectionHead accent="var(--g-a)">▚ SUPPORT</SectionHead>
|
||||
<h1 className="mono" style={{ fontSize: 28, fontWeight: 800, letterSpacing: '-0.02em', margin: '8px 0 18px' }}>HELP & FAQ</h1>
|
||||
|
||||
<div style={{ marginBottom: 20 }}>
|
||||
<TerminalInput prompt="›" value={query} onChange={setQuery} placeholder="Search the manual…" />
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
{filtered.map((f, i) => {
|
||||
const isOpen = open === i;
|
||||
return (
|
||||
<div key={i} className="scanlines" style={{ background: 'var(--bg-1)', border: '1px solid var(--border)', borderRadius: 8, overflow: 'hidden' }}>
|
||||
<button
|
||||
onClick={() => setOpen(isOpen ? null : i)}
|
||||
aria-expanded={isOpen}
|
||||
style={{ width: '100%', display: 'flex', alignItems: 'center', gap: 12, padding: '14px 16px', background: 'transparent', border: 'none', cursor: 'pointer', textAlign: 'left' }}
|
||||
>
|
||||
<span className="label" style={{ color: 'var(--g-a)', fontSize: 9.5 }}>{f.cat}</span>
|
||||
<span style={{ flex: 1, fontSize: 14, fontWeight: 600, color: 'var(--text-0)' }}>{f.q}</span>
|
||||
<span className="mono" style={{ color: 'var(--text-1)' }}>{isOpen ? '−' : '+'}</span>
|
||||
</button>
|
||||
{isOpen && (
|
||||
<div className="mono" style={{ padding: '0 16px 16px', fontSize: 13, color: 'var(--text-1)', lineHeight: 1.65 }}>{f.a}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{filtered.length === 0 && (
|
||||
<div className="mono" style={{ padding: 16, fontSize: 13, color: 'var(--text-1)' }}>No answers matched “{query}”.</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mono" style={{ marginTop: 24, fontSize: 13, color: 'var(--text-1)' }}>
|
||||
Still stuck? <a href="mailto:support@vyndr.app" style={{ color: 'var(--g-a)', textDecoration: 'none' }}>support@vyndr.app</a>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user