Files
vyndr/src/routes/preferences.js
T
builtbykev 3b47b783dc 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>
2026-06-19 10:20:55 -04:00

80 lines
3.0 KiB
JavaScript

'use strict';
/**
* /api/preferences (Session 49) — per-user onboarding preferences.
*
* Stored in Supabase auth `user_metadata.preferences` (NO migration / extra
* table). Auth-gated by the shared `requireAuth` (req.user = the app `users`
* row; req.user.id is the auth user id). Reads/writes via the service client's
* admin API. POST is a PARTIAL merge — only the keys present in the body change.
*
* Shape: { sports: string[], books: string[], weekly_limit: number|null,
* onboarding_complete: boolean }
*/
const express = require('express');
const { requireAuth } = require('../middleware/auth');
const { getSupabaseServiceClient } = require('../utils/supabase');
const router = express.Router();
router.use(requireAuth);
const DEFAULTS = { sports: [], books: [], weekly_limit: null, onboarding_complete: false };
const VALID_SPORTS = new Set(['mlb', 'nba', 'wnba', 'soccer']);
/** Keep only valid keys/values; only present keys are returned (partial). */
function sanitizePrefs(p = {}) {
const out = {};
if (Array.isArray(p.sports)) {
out.sports = [...new Set(p.sports.map((s) => String(s).toLowerCase()).filter((s) => VALID_SPORTS.has(s)))];
}
if (Array.isArray(p.books)) {
out.books = [...new Set(p.books.map((b) => String(b).slice(0, 40)))].slice(0, 12);
}
if (Object.prototype.hasOwnProperty.call(p, 'weekly_limit')) {
const n = Number(p.weekly_limit);
out.weekly_limit = p.weekly_limit === null ? null : Number.isFinite(n) ? Math.max(0, Math.min(100000, n)) : null;
}
if (typeof p.onboarding_complete === 'boolean') out.onboarding_complete = p.onboarding_complete;
return out;
}
async function readPrefs(sb, userId) {
const { data, error } = await sb.auth.admin.getUserById(userId);
if (error) throw error;
const prefs = (data && data.user && data.user.user_metadata && data.user.user_metadata.preferences) || {};
const meta = (data && data.user && data.user.user_metadata) || {};
return { prefs: { ...DEFAULTS, ...prefs }, meta };
}
// GET /api/preferences
router.get('/', async (req, res) => {
try {
const sb = getSupabaseServiceClient();
const { prefs } = await readPrefs(sb, req.user.id);
return res.json(prefs);
} catch (err) {
console.error('[preferences/get]', err.message);
return res.status(503).json({ error: 'Could not load preferences' });
}
});
// POST /api/preferences — partial merge.
router.post('/', async (req, res) => {
try {
const sb = getSupabaseServiceClient();
const { prefs: existing, meta } = await readPrefs(sb, req.user.id);
const merged = { ...existing, ...sanitizePrefs(req.body || {}) };
const { error } = await sb.auth.admin.updateUserById(req.user.id, {
user_metadata: { ...meta, preferences: merged },
});
if (error) throw error;
return res.json({ ok: true, preferences: merged });
} catch (err) {
console.error('[preferences/post]', err.message);
return res.status(503).json({ error: 'Could not save preferences' });
}
});
module.exports = router;