feat(pages): pass user explicitly + null-guard leaky page seeds (Brief 2 of fix-layout-default-user)
Closes the page-side half of P0 #7 from .convoys/ship-readiness.md. Brief 1 (commit ddf8fd2) handled the Layout-side fix. Per the architect's per-page bucket table (Decision B in .convoys/fix-layout-default-user.md), 7 pages needed code changes; the other 10 of 17 Layout-importing pages already pass `user` correctly. Pass user={user} to Layout (4 pages, 11 call sites): - pages/scanner.js (1 call) - pages/decks.js (3 calls) - pages/deck-builder.js (4 calls) - pages/deck/[id].js (3 calls) (All four still import useAuth from lib/auth-context.js — that's intentional and stays as-is until the single-auth-provider convoy collapses the three parallel auth surfaces.) Replace leaky page-level seed values with useState(null) + null guards (2 pages, R2 mitigation): - pages/profile.js: useState({email: 'me@...', role: 'user', ...}) → useState(null) + ?. on every sync user.* read + early-return guards in getDisplayName/getInitials + conditional render around the "Member since" block so formatDate(undefined) never runs - pages/settings.js: same pattern (single user.email reader guarded) Replace hardcoded const with useAuth from lib/use-auth.js (1 page): - pages/card/[id].js: const user = {email: 'me@...'} → const { user } = useAuth() (called unconditionally at the top of the component; rules-of-hooks safe) Verification: - grep 'me@randallstillwell.com' pages/ → 0 hits - 21/21 vitest tests pass (16 pre-existing + 5 from Brief 1) - npm run lint matches baseline (128 problems pre, 128 post; verified via git stash before/after) - Manual static read-through of every diff; ReadLints clean on the 7 files - Dev-server smoke: /cards anonymous returned HTTP 200 with 0 'me@randallstillwell' matches before the user's shared dev server became unresponsive mid-session (same dev-server-shared-by-user constraint flagged in Brief 1); interactive logged-in smoke is parent/operator gated Flagged-but-deferred (untouched per scope): - 4 pages still import useAuth from lib/auth-context.js → single-auth-provider (queued P1 #9) - components/MobileNavigation.js still receives dead user prop → cleanup-mobile-nav-dead-props (or fold into god-component-split) addresses: P0 #7 from .convoys/ship-readiness.md (last P0 ship-blocker) Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
79171e8b97
commit
0f6bfbb7df
7 changed files with 45 additions and 57 deletions
|
|
@ -2,6 +2,7 @@ import { useState, useEffect } from 'react';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import Layout from '../../components/Layout';
|
import Layout from '../../components/Layout';
|
||||||
import { useIsAdmin } from '../../lib/admin-auth';
|
import { useIsAdmin } from '../../lib/admin-auth';
|
||||||
|
import { useAuth } from '../../lib/use-auth';
|
||||||
import CollectionSelectionModal from '../../components/CollectionSelectionModal';
|
import CollectionSelectionModal from '../../components/CollectionSelectionModal';
|
||||||
import { ManaCost, ColorIdentity, AdvancedManaCost } from '../../components/ManaSymbols';
|
import { ManaCost, ColorIdentity, AdvancedManaCost } from '../../components/ManaSymbols';
|
||||||
import ManaSymbolSettings from '../../components/ManaSymbolSettings';
|
import ManaSymbolSettings from '../../components/ManaSymbolSettings';
|
||||||
|
|
@ -9,11 +10,7 @@ import ManaSymbolSettings from '../../components/ManaSymbolSettings';
|
||||||
export default function CardDetail() {
|
export default function CardDetail() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { id } = router.query;
|
const { id } = router.query;
|
||||||
|
const { user } = useAuth();
|
||||||
const user = {
|
|
||||||
email: 'me@randallstillwell.com',
|
|
||||||
role: 'user'
|
|
||||||
};
|
|
||||||
|
|
||||||
const [card, setCard] = useState(null);
|
const [card, setCard] = useState(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
|
||||||
|
|
@ -259,7 +259,7 @@ export default function DeckBuilder() {
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout user={user}>
|
||||||
<div className="flex items-center justify-center min-h-screen">
|
<div className="flex items-center justify-center min-h-screen">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h1 className="text-2xl font-bold mb-4">Please log in to use the deck builder</h1>
|
<h1 className="text-2xl font-bold mb-4">Please log in to use the deck builder</h1>
|
||||||
|
|
@ -274,7 +274,7 @@ export default function DeckBuilder() {
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout user={user}>
|
||||||
<div className="flex items-center justify-center min-h-screen">
|
<div className="flex items-center justify-center min-h-screen">
|
||||||
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-accent-ember"></div>
|
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-accent-ember"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -284,7 +284,7 @@ export default function DeckBuilder() {
|
||||||
|
|
||||||
if (!deck) {
|
if (!deck) {
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout user={user}>
|
||||||
<div className="flex items-center justify-center min-h-screen">
|
<div className="flex items-center justify-center min-h-screen">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h1 className="text-2xl font-bold mb-4">Deck not found</h1>
|
<h1 className="text-2xl font-bold mb-4">Deck not found</h1>
|
||||||
|
|
@ -300,7 +300,7 @@ export default function DeckBuilder() {
|
||||||
const stats = getDeckStats();
|
const stats = getDeckStats();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout user={user}>
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex justify-between items-center mb-6">
|
<div className="flex justify-between items-center mb-6">
|
||||||
|
|
|
||||||
|
|
@ -129,7 +129,7 @@ export default function DeckDetail() {
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout user={user}>
|
||||||
<div className="flex items-center justify-center min-h-screen">
|
<div className="flex items-center justify-center min-h-screen">
|
||||||
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-accent-ember"></div>
|
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-accent-ember"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -139,7 +139,7 @@ export default function DeckDetail() {
|
||||||
|
|
||||||
if (!deck) {
|
if (!deck) {
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout user={user}>
|
||||||
<div className="flex items-center justify-center min-h-screen">
|
<div className="flex items-center justify-center min-h-screen">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h1 className="text-2xl font-bold mb-4">Deck not found</h1>
|
<h1 className="text-2xl font-bold mb-4">Deck not found</h1>
|
||||||
|
|
@ -157,7 +157,7 @@ export default function DeckDetail() {
|
||||||
const isOwner = user && deck.user_id === user.userId;
|
const isOwner = user && deck.user_id === user.userId;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout user={user}>
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex justify-between items-start mb-8">
|
<div className="flex justify-between items-start mb-8">
|
||||||
|
|
|
||||||
|
|
@ -158,7 +158,7 @@ export default function Decks() {
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout user={user}>
|
||||||
<div className="flex items-center justify-center min-h-screen">
|
<div className="flex items-center justify-center min-h-screen">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h1 className="text-2xl font-bold mb-4">Please log in to view your decks</h1>
|
<h1 className="text-2xl font-bold mb-4">Please log in to view your decks</h1>
|
||||||
|
|
@ -173,7 +173,7 @@ export default function Decks() {
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout user={user}>
|
||||||
<div className="flex items-center justify-center min-h-screen">
|
<div className="flex items-center justify-center min-h-screen">
|
||||||
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-accent-ember"></div>
|
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-accent-ember"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -182,7 +182,7 @@ export default function Decks() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout user={user}>
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex justify-between items-center mb-8">
|
<div className="flex justify-between items-center mb-8">
|
||||||
|
|
|
||||||
|
|
@ -7,17 +7,7 @@ export default function Profile() {
|
||||||
const fileInputRef = useRef(null);
|
const fileInputRef = useRef(null);
|
||||||
|
|
||||||
// User state
|
// User state
|
||||||
const [user, setUser] = useState({
|
const [user, setUser] = useState(null);
|
||||||
email: 'me@randallstillwell.com',
|
|
||||||
role: 'admin',
|
|
||||||
first_name: '',
|
|
||||||
last_name: '',
|
|
||||||
username: '',
|
|
||||||
bio: '',
|
|
||||||
avatar_url: '',
|
|
||||||
favorite_games: ['MTG'],
|
|
||||||
created_at: new Date().toISOString()
|
|
||||||
});
|
|
||||||
|
|
||||||
// UI state
|
// UI state
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
@ -236,6 +226,7 @@ export default function Profile() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const getDisplayName = () => {
|
const getDisplayName = () => {
|
||||||
|
if (!user) return '';
|
||||||
if (user.first_name || user.last_name) {
|
if (user.first_name || user.last_name) {
|
||||||
return `${user.first_name} ${user.last_name}`.trim();
|
return `${user.first_name} ${user.last_name}`.trim();
|
||||||
}
|
}
|
||||||
|
|
@ -243,6 +234,7 @@ export default function Profile() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const getInitials = () => {
|
const getInitials = () => {
|
||||||
|
if (!user) return '';
|
||||||
if (user.first_name || user.last_name) {
|
if (user.first_name || user.last_name) {
|
||||||
return `${user.first_name?.charAt(0) || ''}${user.last_name?.charAt(0) || ''}`.toUpperCase();
|
return `${user.first_name?.charAt(0) || ''}${user.last_name?.charAt(0) || ''}`.toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
@ -296,7 +288,7 @@ export default function Profile() {
|
||||||
{/* Avatar */}
|
{/* Avatar */}
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<div className="relative inline-block">
|
<div className="relative inline-block">
|
||||||
{user.avatar_url ? (
|
{user?.avatar_url ? (
|
||||||
<img
|
<img
|
||||||
src={user.avatar_url}
|
src={user.avatar_url}
|
||||||
alt="Profile"
|
alt="Profile"
|
||||||
|
|
@ -358,29 +350,29 @@ export default function Profile() {
|
||||||
<h2 className="text-2xl font-bold mb-1" style={{ color: 'var(--text-primary)' }}>
|
<h2 className="text-2xl font-bold mb-1" style={{ color: 'var(--text-primary)' }}>
|
||||||
{getDisplayName()}
|
{getDisplayName()}
|
||||||
</h2>
|
</h2>
|
||||||
{user.username && (
|
{user?.username && (
|
||||||
<p className="text-lg mb-2" style={{ color: 'var(--text-secondary)' }}>
|
<p className="text-lg mb-2" style={{ color: 'var(--text-secondary)' }}>
|
||||||
@{user.username}
|
@{user.username}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>
|
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>
|
||||||
{user.email}
|
{user?.email}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex items-center justify-center mt-2">
|
<div className="flex items-center justify-center mt-2">
|
||||||
<span
|
<span
|
||||||
className="px-3 py-1 text-xs rounded-full font-medium"
|
className="px-3 py-1 text-xs rounded-full font-medium"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: user.role === 'admin' ? 'var(--accent-ember)' : 'var(--accent-gold)',
|
backgroundColor: user?.role === 'admin' ? 'var(--accent-ember)' : 'var(--accent-gold)',
|
||||||
color: 'white'
|
color: 'white'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{user.role?.toUpperCase()}
|
{user?.role?.toUpperCase()}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Bio */}
|
{/* Bio */}
|
||||||
{user.bio && (
|
{user?.bio && (
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<p className="text-sm leading-relaxed" style={{ color: 'var(--text-secondary)' }}>
|
<p className="text-sm leading-relaxed" style={{ color: 'var(--text-secondary)' }}>
|
||||||
{user.bio}
|
{user.bio}
|
||||||
|
|
@ -389,11 +381,13 @@ export default function Profile() {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Member Since */}
|
{/* Member Since */}
|
||||||
<div className="text-center">
|
{user?.created_at && (
|
||||||
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>
|
<div className="text-center">
|
||||||
Member since {formatDate(user.created_at)}
|
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>
|
||||||
</p>
|
Member since {formatDate(user.created_at)}
|
||||||
</div>
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Stats Card */}
|
{/* Stats Card */}
|
||||||
|
|
@ -468,7 +462,7 @@ export default function Profile() {
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<p className="py-2 text-sm" style={{ color: 'var(--text-secondary)' }}>
|
<p className="py-2 text-sm" style={{ color: 'var(--text-secondary)' }}>
|
||||||
{user.first_name || 'Not set'}
|
{user?.first_name || 'Not set'}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -486,7 +480,7 @@ export default function Profile() {
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<p className="py-2 text-sm" style={{ color: 'var(--text-secondary)' }}>
|
<p className="py-2 text-sm" style={{ color: 'var(--text-secondary)' }}>
|
||||||
{user.last_name || 'Not set'}
|
{user?.last_name || 'Not set'}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -507,7 +501,7 @@ export default function Profile() {
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<p className="py-2 text-sm" style={{ color: 'var(--text-secondary)' }}>
|
<p className="py-2 text-sm" style={{ color: 'var(--text-secondary)' }}>
|
||||||
{user.username || 'Not set'}
|
{user?.username || 'Not set'}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -527,7 +521,7 @@ export default function Profile() {
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<p className="py-2 text-sm leading-relaxed" style={{ color: 'var(--text-secondary)' }}>
|
<p className="py-2 text-sm leading-relaxed" style={{ color: 'var(--text-secondary)' }}>
|
||||||
{user.bio || 'No bio set'}
|
{user?.bio || 'No bio set'}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -546,19 +540,19 @@ export default function Profile() {
|
||||||
className={`px-4 py-2 rounded-xl font-medium transition-all duration-200 flex items-center gap-2 ${
|
className={`px-4 py-2 rounded-xl font-medium transition-all duration-200 flex items-center gap-2 ${
|
||||||
editMode ? 'cursor-pointer hover:shadow-md' : 'cursor-default'
|
editMode ? 'cursor-pointer hover:shadow-md' : 'cursor-default'
|
||||||
} ${
|
} ${
|
||||||
(editMode ? formData.favorite_games : user.favorite_games)?.includes(game.value)
|
(editMode ? formData.favorite_games : user?.favorite_games)?.includes(game.value)
|
||||||
? 'shadow-lg'
|
? 'shadow-lg'
|
||||||
: 'hover:shadow-md'
|
: 'hover:shadow-md'
|
||||||
}`}
|
}`}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: (editMode ? formData.favorite_games : user.favorite_games)?.includes(game.value)
|
backgroundColor: (editMode ? formData.favorite_games : user?.favorite_games)?.includes(game.value)
|
||||||
? 'var(--accent-ember)'
|
? 'var(--accent-ember)'
|
||||||
: 'var(--bg-tertiary)',
|
: 'var(--bg-tertiary)',
|
||||||
color: (editMode ? formData.favorite_games : user.favorite_games)?.includes(game.value)
|
color: (editMode ? formData.favorite_games : user?.favorite_games)?.includes(game.value)
|
||||||
? 'white'
|
? 'white'
|
||||||
: 'var(--text-primary)',
|
: 'var(--text-primary)',
|
||||||
border: `1px solid ${
|
border: `1px solid ${
|
||||||
(editMode ? formData.favorite_games : user.favorite_games)?.includes(game.value)
|
(editMode ? formData.favorite_games : user?.favorite_games)?.includes(game.value)
|
||||||
? 'var(--accent-ember)'
|
? 'var(--accent-ember)'
|
||||||
: 'var(--border)'
|
: 'var(--border)'
|
||||||
}`
|
}`
|
||||||
|
|
@ -578,7 +572,7 @@ export default function Profile() {
|
||||||
</label>
|
</label>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<p className="py-2 text-sm" style={{ color: 'var(--text-secondary)' }}>
|
<p className="py-2 text-sm" style={{ color: 'var(--text-secondary)' }}>
|
||||||
{user.email}
|
{user?.email}
|
||||||
</p>
|
</p>
|
||||||
<span className="px-2 py-1 text-xs rounded-full bg-green-100 dark:bg-green-900/20 text-green-800 dark:text-green-200">
|
<span className="px-2 py-1 text-xs rounded-full bg-green-100 dark:bg-green-900/20 text-green-800 dark:text-green-200">
|
||||||
Verified
|
Verified
|
||||||
|
|
@ -596,11 +590,11 @@ export default function Profile() {
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setEditMode(false);
|
setEditMode(false);
|
||||||
setFormData({
|
setFormData({
|
||||||
first_name: user.first_name || '',
|
first_name: user?.first_name || '',
|
||||||
last_name: user.last_name || '',
|
last_name: user?.last_name || '',
|
||||||
username: user.username || '',
|
username: user?.username || '',
|
||||||
bio: user.bio || '',
|
bio: user?.bio || '',
|
||||||
favorite_games: user.favorite_games || []
|
favorite_games: user?.favorite_games || []
|
||||||
});
|
});
|
||||||
setMessage({ type: '', text: '' });
|
setMessage({ type: '', text: '' });
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -330,7 +330,7 @@ export default function Scanner() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout user={user}>
|
||||||
<div className="h-full flex flex-col">
|
<div className="h-full flex flex-col">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="px-6 pt-6 pb-4">
|
<div className="px-6 pt-6 pb-4">
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,7 @@ export default function Settings() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
// User state
|
// User state
|
||||||
const [user, setUser] = useState({
|
const [user, setUser] = useState(null);
|
||||||
email: 'me@randallstillwell.com',
|
|
||||||
role: 'admin'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Settings state
|
// Settings state
|
||||||
const [settings, setSettings] = useState({
|
const [settings, setSettings] = useState({
|
||||||
|
|
@ -303,7 +300,7 @@ export default function Settings() {
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<input
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
value={user.email}
|
value={user?.email || ''}
|
||||||
disabled
|
disabled
|
||||||
className="input-field flex-1 opacity-50 cursor-not-allowed"
|
className="input-field flex-1 opacity-50 cursor-not-allowed"
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue