Session 49: Complete onboarding flow + name micro-fixes (2185 tests)
Name micro-fixes (close the normalization arc): - collapseInitials merges "J C Escarra" -> "JC Escarra" (display + key); both playerName.js copies. Added mickey:michael nickname. Onboarding flow (end-to-end, complete): - Storage: Supabase user_metadata.preferences (no migration). - API: src/routes/preferences.js GET/POST (requireAuth, admin getUserById/ updateUserById, partial merge + sanitize) + Next /api/preferences proxy. - Page: web onboarding/page.tsx — 3 steps (sports >=1 / books skip / bankroll presets+custom+skip) -> SIGNAL ACTIVE -> POST onboarding_complete:true -> 2s -> /dashboard. Redirects to login when unauthenticated. - Redirect: dashboard fetches /api/preferences fresh; new+incomplete users (created_at >= cutoff) -> /onboarding; never while auth loading; existing users exempt. - Personalization: Slate default tab = prefs.sports[0]; preferred books glow in the card lines grid (lib/books isPreferredBook, threaded dash->Slate->GameCard). - Settings: PREFERENCES section loads + edits + saves sports/books/limit. Backend 2156 -> 2185 tests (+29), 184 suites. Web build clean (exit 0). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -346,9 +346,11 @@ function groupByGame(rawProps: RawProp[], sport: SlateSport): SlateGame[] {
|
||||
export interface SlateProps {
|
||||
initialTab?: SlateTab;
|
||||
tier?: Tier;
|
||||
/** Session 49 — user's preferred books (highlighted in each card's lines). */
|
||||
preferredBooks?: string[];
|
||||
}
|
||||
|
||||
export default function Slate({ initialTab = 'all', tier = 'free' }: SlateProps) {
|
||||
export default function Slate({ initialTab = 'all', tier = 'free', preferredBooks }: SlateProps) {
|
||||
const router = useRouter();
|
||||
const { session } = useAuth();
|
||||
const [tab, setTab] = useState<SlateTab>(initialTab);
|
||||
@@ -751,6 +753,7 @@ export default function Slate({ initialTab = 'all', tier = 'free' }: SlateProps)
|
||||
<VyndrGameCard
|
||||
key={`${g.sport}-${g.homeTeam}-${g.awayTeam}-${i}`}
|
||||
game={slateGameToCardData(g, gradeIndex, deltaIndex, pitcherMap)}
|
||||
preferredBooks={preferredBooks}
|
||||
onOpen={() => router.push('/scan')}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -6,6 +6,7 @@ import GradeBadge from '@/components/vyndr/GradeBadge';
|
||||
import ArchetypeBadge from '@/components/vyndr/ArchetypeBadge';
|
||||
import StatStrip, { type StatCell, type StripProp, type StripArchetype } from '@/components/vyndr/StatStrip';
|
||||
import { playerHref } from '@/lib/playerHref';
|
||||
import { isPreferredBook } from '@/lib/books';
|
||||
|
||||
export interface GameLine {
|
||||
book: string;
|
||||
@@ -62,6 +63,8 @@ interface GameCardProps {
|
||||
game: GameCardData;
|
||||
onAddParlay?: (p: GameProp) => void;
|
||||
onOpen?: (id: string) => void;
|
||||
/** Session 49 — the user's preferred books, highlighted in the lines grid. */
|
||||
preferredBooks?: string[];
|
||||
}
|
||||
|
||||
/** A book-line cell with the Bloomberg pattern: best = green tint + green left
|
||||
@@ -116,7 +119,7 @@ function PropRow({ prop: p, onAddParlay }: { prop: GameProp; onAddParlay?: (p: G
|
||||
|
||||
/** Dashboard / Slate game card (§7) — game-lines grid w/ best-line highlight,
|
||||
* graded props, inline streaks, live indicator. */
|
||||
export default function GameCard({ game: g, onAddParlay, onOpen }: GameCardProps) {
|
||||
export default function GameCard({ game: g, onAddParlay, onOpen, preferredBooks }: GameCardProps) {
|
||||
return (
|
||||
<div className="scanlines" style={{ background: 'var(--bg-1)', border: '1px solid var(--border)', borderRadius: 10, overflow: 'hidden' }}>
|
||||
{/* HEADER */}
|
||||
@@ -178,7 +181,7 @@ export default function GameCard({ game: g, onAddParlay, onOpen }: GameCardProps
|
||||
<div className="label" style={{ fontSize: 10, textAlign: 'center' }}>O/U</div>
|
||||
{g.lines.map((ln, i) => (
|
||||
<span key={i} style={{ display: 'contents' }}>
|
||||
<div className="mono" style={{ fontSize: 12, fontWeight: 700, color: 'var(--text-1)', paddingLeft: 2 }}>{ln.book}</div>
|
||||
<div className="mono" style={{ fontSize: 12, fontWeight: 700, color: isPreferredBook(ln.book, preferredBooks) ? 'var(--g-a)' : 'var(--text-1)', paddingLeft: 2, textShadow: isPreferredBook(ln.book, preferredBooks) ? '0 0 8px rgba(0,212,160,.5)' : 'none' }} title={isPreferredBook(ln.book, preferredBooks) ? 'Your book' : undefined}>{ln.book}</div>
|
||||
<LineCell value={ln.awayML} best={ln.bestAway} worst={ln.worstAway} />
|
||||
<LineCell value={ln.homeML} best={ln.bestHome} worst={ln.worstHome} />
|
||||
<LineCell value={ln.ou} best={ln.bestOU} />
|
||||
|
||||
Reference in New Issue
Block a user