3b47b783dc
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>
95 lines
3.8 KiB
JavaScript
95 lines
3.8 KiB
JavaScript
// Session 49 — onboarding flow: page, redirect, personalization, settings, book
|
|
// highlight. Page logic is asserted as source (the journey is structural);
|
|
// book-matching logic runs directly.
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const WEB = path.join(__dirname, '..', '..', 'web', 'src');
|
|
const read = (rel) => fs.readFileSync(path.join(WEB, rel), 'utf8');
|
|
const books = require('../../web/src/lib/books');
|
|
|
|
describe('isPreferredBook (book matching)', () => {
|
|
it('matches a book across id / code / name forms', () => {
|
|
expect(books.isPreferredBook('DK', ['draftkings'])).toBe(true);
|
|
expect(books.isPreferredBook('DraftKings', ['draftkings'])).toBe(true);
|
|
expect(books.isPreferredBook('draftkings', ['DK'])).toBe(true);
|
|
expect(books.isPreferredBook('FD', ['draftkings'])).toBe(false);
|
|
});
|
|
it('returns false for empty/missing preferences', () => {
|
|
expect(books.isPreferredBook('DK', [])).toBe(false);
|
|
expect(books.isPreferredBook('DK', undefined)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('Onboarding page', () => {
|
|
const src = read('app/onboarding/page.tsx');
|
|
it('renders 3 steps + a completion step', () => {
|
|
expect(src).toContain('data-step="1"');
|
|
expect(src).toContain('data-step="2"');
|
|
expect(src).toContain('data-step="3"');
|
|
expect(src).toContain('SIGNAL ACTIVE');
|
|
expect(src).toContain("You're locked in");
|
|
});
|
|
it('step 1 requires at least one sport', () => {
|
|
expect(src).toContain('disabled={sports.length === 0}');
|
|
});
|
|
it('saves preferences via POST with onboarding_complete:true → /dashboard', () => {
|
|
expect(src).toContain("fetch('/api/preferences'");
|
|
expect(src).toContain('onboarding_complete: true');
|
|
expect(src).toContain("router.replace('/dashboard')");
|
|
});
|
|
it('redirects to login when not signed in (never to onboarding)', () => {
|
|
expect(src).toContain("router.replace('/login?next=/onboarding')");
|
|
});
|
|
});
|
|
|
|
describe('Dashboard onboarding redirect + personalization', () => {
|
|
const src = read('app/dashboard/page.tsx');
|
|
it('fetches prefs and redirects new incomplete users to /onboarding', () => {
|
|
expect(src).toContain("fetch('/api/preferences'");
|
|
expect(src).toContain("router.replace('/onboarding')");
|
|
expect(src).toContain('onboarding_complete !== true');
|
|
});
|
|
it('exempts existing users via created_at cutoff', () => {
|
|
expect(src).toContain('ONBOARDING_CUTOFF');
|
|
expect(src).toContain('isNewUser');
|
|
});
|
|
it('does not redirect while auth is loading', () => {
|
|
expect(src).toContain('if (authLoading || !user || !session?.access_token) return;');
|
|
});
|
|
it('passes primary sport tab + preferred books to the Slate', () => {
|
|
expect(src).toContain('initialTab={primaryTab}');
|
|
expect(src).toContain('preferredBooks={prefBooks}');
|
|
});
|
|
});
|
|
|
|
describe('Slate + GameCard thread preferred books', () => {
|
|
it('Slate accepts + forwards preferredBooks', () => {
|
|
const src = read('components/Slate.tsx');
|
|
expect(src).toContain('preferredBooks');
|
|
expect(src).toContain('preferredBooks={preferredBooks}');
|
|
});
|
|
it('GameCard highlights the preferred book row', () => {
|
|
const src = read('components/vyndr/GameCard.tsx');
|
|
expect(src).toContain('isPreferredBook(ln.book, preferredBooks)');
|
|
});
|
|
});
|
|
|
|
describe('Settings preferences section', () => {
|
|
const src = read('app/settings/page.tsx');
|
|
it('renders a PREFERENCES section that loads + saves prefs', () => {
|
|
expect(src).toContain('label="PREFERENCES"');
|
|
expect(src).toContain("fetch('/api/preferences'");
|
|
expect(src).toContain('savePrefs');
|
|
});
|
|
});
|
|
|
|
describe('Preferences Next proxy', () => {
|
|
it('forwards GET + POST with auth', () => {
|
|
const src = read('app/api/preferences/route.ts');
|
|
expect(src).toContain('export async function GET');
|
|
expect(src).toContain('export async function POST');
|
|
expect(src).toContain('Authorization');
|
|
});
|
|
});
|