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:
Kev
2026-06-19 10:20:55 -04:00
parent 91b03c4044
commit 3b47b783dc
17 changed files with 687 additions and 17 deletions
+30 -2
View File
@@ -75,7 +75,7 @@ const SPORT_COLOR: Record<Sport, string> = {
export default function DashboardPage() {
const router = useRouter();
const { user, tier, scansRemaining, loading: authLoading } = useAuth();
const { user, session, tier, scansRemaining, loading: authLoading } = useAuth();
const { addLeg, open } = useParlay();
const [sport, setSport] = useState<Sport>('NBA');
@@ -83,12 +83,40 @@ export default function DashboardPage() {
const [topGrades, setTopGrades] = useState<TopGrade[] | null>(null);
const [mostParlayed, setMostParlayed] = useState<ParlayLegStat[] | null>(null);
const [recentScans, setRecentScans] = useState<RecentScan[] | null>(null);
// Session 49 — the user's primary sport tab + preferred books from prefs.
const [primaryTab, setPrimaryTab] = useState<'all' | 'nba' | 'mlb' | 'wnba' | 'soccer'>('all');
const [prefBooks, setPrefBooks] = useState<string[]>([]);
// Gate
useEffect(() => {
if (!authLoading && !user) router.replace('/login?next=/dashboard');
}, [authLoading, user, router]);
// Session 49 — onboarding gate + dashboard personalization. New users
// (created after onboarding shipped) who haven't completed it are sent to
// /onboarding; everyone else's saved preferences set the default sport tab.
// Never fires while auth is loading (would bounce unauthenticated users).
useEffect(() => {
if (authLoading || !user || !session?.access_token) return;
let active = true;
fetch('/api/preferences', { headers: { Authorization: `Bearer ${session.access_token}` } })
.then((r) => (r.ok ? r.json() : null))
.then((prefs) => {
if (!active || !prefs) return;
const createdAt = session.user?.created_at ? new Date(session.user.created_at).getTime() : 0;
const ONBOARDING_CUTOFF = new Date('2026-06-19T00:00:00Z').getTime();
const isNewUser = createdAt >= ONBOARDING_CUTOFF;
if (prefs.onboarding_complete !== true && isNewUser) {
router.replace('/onboarding');
return;
}
if (Array.isArray(prefs.sports) && prefs.sports[0]) setPrimaryTab(prefs.sports[0]);
if (Array.isArray(prefs.books)) setPrefBooks(prefs.books);
})
.catch(() => { /* prefs are best-effort — never block the dashboard */ });
return () => { active = false; };
}, [authLoading, user, session, router]);
// Fetch slate when sport changes
useEffect(() => {
let cancelled = false;
@@ -198,7 +226,7 @@ export default function DashboardPage() {
search, and inline grading. Renders ABOVE the existing
intelligence sections (Top Graded / Most Parlayed / Recent
Reads) which serve as supplementary surfaces. */}
<Slate tier={tier} />
<Slate tier={tier} initialTab={primaryTab} preferredBooks={prefBooks} key={primaryTab} />
{/* Legacy sport tabs — supplementary, kept for the existing
Top Graded / Most Parlayed flows below. */}