625 lines
23 KiB
JavaScript
625 lines
23 KiB
JavaScript
|
|
import { useState, useEffect, useRef } from 'react';
|
||
|
|
import { useRouter } from 'next/router';
|
||
|
|
import Layout from '../components/Layout';
|
||
|
|
|
||
|
|
export default function Profile() {
|
||
|
|
const router = useRouter();
|
||
|
|
const fileInputRef = useRef(null);
|
||
|
|
|
||
|
|
// User state
|
||
|
|
const [user, setUser] = useState({
|
||
|
|
email: 'me@randallstillwell.com',
|
||
|
|
role: 'admin',
|
||
|
|
first_name: '',
|
||
|
|
last_name: '',
|
||
|
|
username: '',
|
||
|
|
bio: '',
|
||
|
|
avatar_url: '',
|
||
|
|
favorite_games: ['MTG'],
|
||
|
|
created_at: new Date().toISOString()
|
||
|
|
});
|
||
|
|
|
||
|
|
// UI state
|
||
|
|
const [loading, setLoading] = useState(true);
|
||
|
|
const [saving, setSaving] = useState(false);
|
||
|
|
const [editMode, setEditMode] = useState(false);
|
||
|
|
const [message, setMessage] = useState({ type: '', text: '' });
|
||
|
|
const [stats, setStats] = useState({
|
||
|
|
totalCards: 0,
|
||
|
|
totalCollections: 0,
|
||
|
|
totalDecks: 0,
|
||
|
|
totalValue: 0
|
||
|
|
});
|
||
|
|
|
||
|
|
// Form state
|
||
|
|
const [formData, setFormData] = useState({
|
||
|
|
first_name: '',
|
||
|
|
last_name: '',
|
||
|
|
username: '',
|
||
|
|
bio: '',
|
||
|
|
favorite_games: []
|
||
|
|
});
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
loadUserProfile();
|
||
|
|
loadUserStats();
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
const loadUserProfile = async () => {
|
||
|
|
try {
|
||
|
|
const token = localStorage.getItem('auth_token');
|
||
|
|
if (!token) {
|
||
|
|
router.push('/login');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const response = await fetch('/api/user/profile', {
|
||
|
|
headers: {
|
||
|
|
'Authorization': `Bearer ${token}`
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
if (response.ok) {
|
||
|
|
const userData = await response.json();
|
||
|
|
setUser(userData);
|
||
|
|
setFormData({
|
||
|
|
first_name: userData.first_name || '',
|
||
|
|
last_name: userData.last_name || '',
|
||
|
|
username: userData.username || '',
|
||
|
|
bio: userData.bio || '',
|
||
|
|
favorite_games: userData.favorite_games || []
|
||
|
|
});
|
||
|
|
} else if (response.status === 401) {
|
||
|
|
router.push('/login');
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Error loading profile:', error);
|
||
|
|
setMessage({ type: 'error', text: 'Failed to load profile' });
|
||
|
|
} finally {
|
||
|
|
setLoading(false);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const loadUserStats = async () => {
|
||
|
|
try {
|
||
|
|
const token = localStorage.getItem('auth_token');
|
||
|
|
if (!token) return;
|
||
|
|
|
||
|
|
const response = await fetch('/api/user/stats', {
|
||
|
|
headers: {
|
||
|
|
'Authorization': `Bearer ${token}`
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
if (response.ok) {
|
||
|
|
const statsData = await response.json();
|
||
|
|
setStats(statsData);
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Error loading stats:', error);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleInputChange = (field, value) => {
|
||
|
|
setFormData(prev => ({
|
||
|
|
...prev,
|
||
|
|
[field]: value
|
||
|
|
}));
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleGameToggle = (game) => {
|
||
|
|
setFormData(prev => ({
|
||
|
|
...prev,
|
||
|
|
favorite_games: prev.favorite_games.includes(game)
|
||
|
|
? prev.favorite_games.filter(g => g !== game)
|
||
|
|
: [...prev.favorite_games, game]
|
||
|
|
}));
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleSave = async () => {
|
||
|
|
setSaving(true);
|
||
|
|
setMessage({ type: '', text: '' });
|
||
|
|
|
||
|
|
try {
|
||
|
|
const token = localStorage.getItem('auth_token');
|
||
|
|
const response = await fetch('/api/user/profile', {
|
||
|
|
method: 'PUT',
|
||
|
|
headers: {
|
||
|
|
'Content-Type': 'application/json',
|
||
|
|
'Authorization': `Bearer ${token}`
|
||
|
|
},
|
||
|
|
body: JSON.stringify(formData)
|
||
|
|
});
|
||
|
|
|
||
|
|
if (response.ok) {
|
||
|
|
const updatedUser = await response.json();
|
||
|
|
setUser(updatedUser);
|
||
|
|
setEditMode(false);
|
||
|
|
setMessage({ type: 'success', text: 'Profile updated successfully!' });
|
||
|
|
} else {
|
||
|
|
const error = await response.json();
|
||
|
|
setMessage({ type: 'error', text: error.message || 'Failed to update profile' });
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Error updating profile:', error);
|
||
|
|
setMessage({ type: 'error', text: 'Failed to update profile' });
|
||
|
|
} finally {
|
||
|
|
setSaving(false);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleAvatarUpload = async (event) => {
|
||
|
|
const file = event.target.files[0];
|
||
|
|
if (!file) return;
|
||
|
|
|
||
|
|
// Validate file type and size
|
||
|
|
if (!file.type.startsWith('image/')) {
|
||
|
|
setMessage({ type: 'error', text: 'Please select an image file' });
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (file.size > 5 * 1024 * 1024) { // 5MB limit
|
||
|
|
setMessage({ type: 'error', text: 'Image must be less than 5MB' });
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const formData = new FormData();
|
||
|
|
formData.append('avatar', file);
|
||
|
|
|
||
|
|
try {
|
||
|
|
setSaving(true);
|
||
|
|
const token = localStorage.getItem('auth_token');
|
||
|
|
const response = await fetch('/api/user/avatar', {
|
||
|
|
method: 'POST',
|
||
|
|
headers: {
|
||
|
|
'Authorization': `Bearer ${token}`
|
||
|
|
},
|
||
|
|
body: formData
|
||
|
|
});
|
||
|
|
|
||
|
|
if (response.ok) {
|
||
|
|
const result = await response.json();
|
||
|
|
setUser(prev => ({ ...prev, avatar_url: result.avatar_url }));
|
||
|
|
setMessage({ type: 'success', text: 'Avatar updated successfully!' });
|
||
|
|
} else {
|
||
|
|
const error = await response.json();
|
||
|
|
setMessage({ type: 'error', text: error.message || 'Failed to upload avatar' });
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Error uploading avatar:', error);
|
||
|
|
setMessage({ type: 'error', text: 'Failed to upload avatar' });
|
||
|
|
} finally {
|
||
|
|
setSaving(false);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const generateAvatar = async () => {
|
||
|
|
try {
|
||
|
|
setSaving(true);
|
||
|
|
const token = localStorage.getItem('auth_token');
|
||
|
|
const response = await fetch('/api/user/avatar/generate', {
|
||
|
|
method: 'POST',
|
||
|
|
headers: {
|
||
|
|
'Authorization': `Bearer ${token}`
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
if (response.ok) {
|
||
|
|
const result = await response.json();
|
||
|
|
setUser(prev => ({ ...prev, avatar_url: result.avatar_url }));
|
||
|
|
setMessage({ type: 'success', text: 'Avatar generated successfully!' });
|
||
|
|
} else {
|
||
|
|
const error = await response.json();
|
||
|
|
setMessage({ type: 'error', text: error.message || 'Failed to generate avatar' });
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Error generating avatar:', error);
|
||
|
|
setMessage({ type: 'error', text: 'Failed to generate avatar' });
|
||
|
|
} finally {
|
||
|
|
setSaving(false);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const formatCurrency = (amount) => {
|
||
|
|
return new Intl.NumberFormat('en-US', {
|
||
|
|
style: 'currency',
|
||
|
|
currency: 'USD'
|
||
|
|
}).format(amount);
|
||
|
|
};
|
||
|
|
|
||
|
|
const formatDate = (dateString) => {
|
||
|
|
return new Date(dateString).toLocaleDateString('en-US', {
|
||
|
|
year: 'numeric',
|
||
|
|
month: 'long',
|
||
|
|
day: 'numeric'
|
||
|
|
});
|
||
|
|
};
|
||
|
|
|
||
|
|
const getDisplayName = () => {
|
||
|
|
if (user.first_name || user.last_name) {
|
||
|
|
return `${user.first_name} ${user.last_name}`.trim();
|
||
|
|
}
|
||
|
|
return user.username || user.email;
|
||
|
|
};
|
||
|
|
|
||
|
|
const getInitials = () => {
|
||
|
|
if (user.first_name || user.last_name) {
|
||
|
|
return `${user.first_name?.charAt(0) || ''}${user.last_name?.charAt(0) || ''}`.toUpperCase();
|
||
|
|
}
|
||
|
|
return user.email?.charAt(0).toUpperCase() || 'U';
|
||
|
|
};
|
||
|
|
|
||
|
|
const gameOptions = [
|
||
|
|
{ value: 'MTG', label: 'Magic: The Gathering', color: 'purple', icon: '🔮' },
|
||
|
|
{ value: 'Pokemon', label: 'Pokemon', color: 'blue', icon: '⚡' },
|
||
|
|
{ value: 'Lorcana', label: 'Disney Lorcana', color: 'pink', icon: '✨' }
|
||
|
|
];
|
||
|
|
|
||
|
|
if (loading) {
|
||
|
|
return (
|
||
|
|
<Layout user={user}>
|
||
|
|
<div className="flex items-center justify-center min-h-screen">
|
||
|
|
<div className="animate-spin rounded-full h-32 w-32 border-b-2" style={{ borderColor: 'var(--accent-ember)' }}></div>
|
||
|
|
</div>
|
||
|
|
</Layout>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<Layout user={user}>
|
||
|
|
<div className="max-w-4xl mx-auto p-6">
|
||
|
|
{/* Header */}
|
||
|
|
<div className="mb-8">
|
||
|
|
<h1 className="text-3xl font-bold mb-2" style={{ color: 'var(--text-primary)' }}>
|
||
|
|
Profile
|
||
|
|
</h1>
|
||
|
|
<p className="text-lg" style={{ color: 'var(--text-secondary)' }}>
|
||
|
|
Manage your account information and preferences
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Message */}
|
||
|
|
{message.text && (
|
||
|
|
<div className={`mb-6 p-4 rounded-xl ${
|
||
|
|
message.type === 'success'
|
||
|
|
? 'bg-green-50 dark:bg-green-900/20 text-green-800 dark:text-green-200 border border-green-200 dark:border-green-800'
|
||
|
|
: 'bg-red-50 dark:bg-red-900/20 text-red-800 dark:text-red-200 border border-red-200 dark:border-red-800'
|
||
|
|
}`}>
|
||
|
|
{message.text}
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||
|
|
{/* Profile Card */}
|
||
|
|
<div className="lg:col-span-1">
|
||
|
|
<div className="card p-6 text-center">
|
||
|
|
{/* Avatar */}
|
||
|
|
<div className="mb-6">
|
||
|
|
<div className="relative inline-block">
|
||
|
|
{user.avatar_url ? (
|
||
|
|
<img
|
||
|
|
src={user.avatar_url}
|
||
|
|
alt="Profile"
|
||
|
|
className="w-32 h-32 rounded-full object-cover mx-auto border-4"
|
||
|
|
style={{ borderColor: 'var(--accent-ember)' }}
|
||
|
|
/>
|
||
|
|
) : (
|
||
|
|
<div
|
||
|
|
className="w-32 h-32 rounded-full flex items-center justify-center mx-auto text-4xl font-bold text-white border-4"
|
||
|
|
style={{
|
||
|
|
backgroundColor: 'var(--accent-ember)',
|
||
|
|
borderColor: 'var(--accent-flame)'
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
{getInitials()}
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{/* Avatar Upload Button */}
|
||
|
|
<button
|
||
|
|
onClick={() => fileInputRef.current?.click()}
|
||
|
|
disabled={saving}
|
||
|
|
className="absolute bottom-0 right-0 p-2 rounded-full shadow-lg transition-all duration-200 hover:scale-110"
|
||
|
|
style={{ backgroundColor: 'var(--accent-flame)', color: 'white' }}
|
||
|
|
title="Upload new avatar"
|
||
|
|
>
|
||
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z" />
|
||
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 13a3 3 0 11-6 0 3 3 0 016 0z" />
|
||
|
|
</svg>
|
||
|
|
</button>
|
||
|
|
|
||
|
|
<input
|
||
|
|
ref={fileInputRef}
|
||
|
|
type="file"
|
||
|
|
accept="image/*"
|
||
|
|
onChange={handleAvatarUpload}
|
||
|
|
className="hidden"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Generate Avatar Button */}
|
||
|
|
<button
|
||
|
|
onClick={generateAvatar}
|
||
|
|
disabled={saving}
|
||
|
|
className="mt-4 px-4 py-2 rounded-xl font-medium transition-all duration-200 hover:shadow-md"
|
||
|
|
style={{
|
||
|
|
backgroundColor: 'var(--bg-tertiary)',
|
||
|
|
color: 'var(--text-primary)',
|
||
|
|
border: '1px solid var(--border)'
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
Generate Avatar
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Basic Info */}
|
||
|
|
<div className="mb-6">
|
||
|
|
<h2 className="text-2xl font-bold mb-1" style={{ color: 'var(--text-primary)' }}>
|
||
|
|
{getDisplayName()}
|
||
|
|
</h2>
|
||
|
|
{user.username && (
|
||
|
|
<p className="text-lg mb-2" style={{ color: 'var(--text-secondary)' }}>
|
||
|
|
@{user.username}
|
||
|
|
</p>
|
||
|
|
)}
|
||
|
|
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>
|
||
|
|
{user.email}
|
||
|
|
</p>
|
||
|
|
<div className="flex items-center justify-center mt-2">
|
||
|
|
<span
|
||
|
|
className="px-3 py-1 text-xs rounded-full font-medium"
|
||
|
|
style={{
|
||
|
|
backgroundColor: user.role === 'admin' ? 'var(--accent-ember)' : 'var(--accent-gold)',
|
||
|
|
color: 'white'
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
{user.role?.toUpperCase()}
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Bio */}
|
||
|
|
{user.bio && (
|
||
|
|
<div className="mb-6">
|
||
|
|
<p className="text-sm leading-relaxed" style={{ color: 'var(--text-secondary)' }}>
|
||
|
|
{user.bio}
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{/* Member Since */}
|
||
|
|
<div className="text-center">
|
||
|
|
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>
|
||
|
|
Member since {formatDate(user.created_at)}
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Stats Card */}
|
||
|
|
<div className="card p-6 mt-6">
|
||
|
|
<h3 className="text-lg font-semibold mb-4" style={{ color: 'var(--text-primary)' }}>
|
||
|
|
Collection Stats
|
||
|
|
</h3>
|
||
|
|
<div className="space-y-4">
|
||
|
|
<div className="flex justify-between items-center">
|
||
|
|
<span style={{ color: 'var(--text-secondary)' }}>Total Cards</span>
|
||
|
|
<span className="font-semibold" style={{ color: 'var(--text-primary)' }}>
|
||
|
|
{stats.totalCards.toLocaleString()}
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
<div className="flex justify-between items-center">
|
||
|
|
<span style={{ color: 'var(--text-secondary)' }}>Collections</span>
|
||
|
|
<span className="font-semibold" style={{ color: 'var(--text-primary)' }}>
|
||
|
|
{stats.totalCollections}
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
<div className="flex justify-between items-center">
|
||
|
|
<span style={{ color: 'var(--text-secondary)' }}>Decks</span>
|
||
|
|
<span className="font-semibold" style={{ color: 'var(--text-primary)' }}>
|
||
|
|
{stats.totalDecks}
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
<div className="flex justify-between items-center pt-2 border-t" style={{ borderColor: 'var(--border)' }}>
|
||
|
|
<span style={{ color: 'var(--text-secondary)' }}>Total Value</span>
|
||
|
|
<span className="font-bold text-lg" style={{ color: 'var(--accent-ember)' }}>
|
||
|
|
{formatCurrency(stats.totalValue)}
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Profile Form */}
|
||
|
|
<div className="lg:col-span-2">
|
||
|
|
<div className="card p-6">
|
||
|
|
<div className="flex justify-between items-center mb-6">
|
||
|
|
<h3 className="text-xl font-semibold" style={{ color: 'var(--text-primary)' }}>
|
||
|
|
Profile Information
|
||
|
|
</h3>
|
||
|
|
<button
|
||
|
|
onClick={() => editMode ? handleSave() : setEditMode(true)}
|
||
|
|
disabled={saving}
|
||
|
|
className="px-4 py-2 rounded-xl font-medium transition-all duration-200 hover:shadow-md"
|
||
|
|
style={{
|
||
|
|
backgroundColor: editMode ? 'var(--accent-ember)' : 'var(--bg-tertiary)',
|
||
|
|
color: editMode ? 'white' : 'var(--text-primary)',
|
||
|
|
border: `1px solid ${editMode ? 'var(--accent-ember)' : 'var(--border)'}`
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
{saving ? 'Saving...' : editMode ? 'Save Changes' : 'Edit Profile'}
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="space-y-6">
|
||
|
|
{/* Name Fields */}
|
||
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
|
|
<div>
|
||
|
|
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
|
||
|
|
First Name
|
||
|
|
</label>
|
||
|
|
{editMode ? (
|
||
|
|
<input
|
||
|
|
type="text"
|
||
|
|
value={formData.first_name}
|
||
|
|
onChange={(e) => handleInputChange('first_name', e.target.value)}
|
||
|
|
className="input-field w-full"
|
||
|
|
placeholder="Enter your first name"
|
||
|
|
/>
|
||
|
|
) : (
|
||
|
|
<p className="py-2 text-sm" style={{ color: 'var(--text-secondary)' }}>
|
||
|
|
{user.first_name || 'Not set'}
|
||
|
|
</p>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
|
||
|
|
Last Name
|
||
|
|
</label>
|
||
|
|
{editMode ? (
|
||
|
|
<input
|
||
|
|
type="text"
|
||
|
|
value={formData.last_name}
|
||
|
|
onChange={(e) => handleInputChange('last_name', e.target.value)}
|
||
|
|
className="input-field w-full"
|
||
|
|
placeholder="Enter your last name"
|
||
|
|
/>
|
||
|
|
) : (
|
||
|
|
<p className="py-2 text-sm" style={{ color: 'var(--text-secondary)' }}>
|
||
|
|
{user.last_name || 'Not set'}
|
||
|
|
</p>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Username */}
|
||
|
|
<div>
|
||
|
|
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
|
||
|
|
Username
|
||
|
|
</label>
|
||
|
|
{editMode ? (
|
||
|
|
<input
|
||
|
|
type="text"
|
||
|
|
value={formData.username}
|
||
|
|
onChange={(e) => handleInputChange('username', e.target.value)}
|
||
|
|
className="input-field w-full"
|
||
|
|
placeholder="Choose a unique username"
|
||
|
|
/>
|
||
|
|
) : (
|
||
|
|
<p className="py-2 text-sm" style={{ color: 'var(--text-secondary)' }}>
|
||
|
|
{user.username || 'Not set'}
|
||
|
|
</p>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Bio */}
|
||
|
|
<div>
|
||
|
|
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
|
||
|
|
Bio
|
||
|
|
</label>
|
||
|
|
{editMode ? (
|
||
|
|
<textarea
|
||
|
|
value={formData.bio}
|
||
|
|
onChange={(e) => handleInputChange('bio', e.target.value)}
|
||
|
|
className="input-field w-full h-24 resize-none"
|
||
|
|
placeholder="Tell us about yourself..."
|
||
|
|
maxLength={500}
|
||
|
|
/>
|
||
|
|
) : (
|
||
|
|
<p className="py-2 text-sm leading-relaxed" style={{ color: 'var(--text-secondary)' }}>
|
||
|
|
{user.bio || 'No bio set'}
|
||
|
|
</p>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Favorite Games */}
|
||
|
|
<div>
|
||
|
|
<label className="block text-sm font-medium mb-3" style={{ color: 'var(--text-primary)' }}>
|
||
|
|
Favorite Games
|
||
|
|
</label>
|
||
|
|
<div className="flex flex-wrap gap-3">
|
||
|
|
{gameOptions.map(game => (
|
||
|
|
<button
|
||
|
|
key={game.value}
|
||
|
|
onClick={() => editMode && handleGameToggle(game.value)}
|
||
|
|
disabled={!editMode}
|
||
|
|
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 ? formData.favorite_games : user.favorite_games)?.includes(game.value)
|
||
|
|
? 'shadow-lg'
|
||
|
|
: 'hover:shadow-md'
|
||
|
|
}`}
|
||
|
|
style={{
|
||
|
|
backgroundColor: (editMode ? formData.favorite_games : user.favorite_games)?.includes(game.value)
|
||
|
|
? 'var(--accent-ember)'
|
||
|
|
: 'var(--bg-tertiary)',
|
||
|
|
color: (editMode ? formData.favorite_games : user.favorite_games)?.includes(game.value)
|
||
|
|
? 'white'
|
||
|
|
: 'var(--text-primary)',
|
||
|
|
border: `1px solid ${
|
||
|
|
(editMode ? formData.favorite_games : user.favorite_games)?.includes(game.value)
|
||
|
|
? 'var(--accent-ember)'
|
||
|
|
: 'var(--border)'
|
||
|
|
}`
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
<span>{game.icon}</span>
|
||
|
|
{game.label}
|
||
|
|
</button>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Email (Read-only) */}
|
||
|
|
<div>
|
||
|
|
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
|
||
|
|
Email Address
|
||
|
|
</label>
|
||
|
|
<div className="flex items-center gap-3">
|
||
|
|
<p className="py-2 text-sm" style={{ color: 'var(--text-secondary)' }}>
|
||
|
|
{user.email}
|
||
|
|
</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">
|
||
|
|
Verified
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
<p className="text-xs mt-1" style={{ color: 'var(--text-secondary)' }}>
|
||
|
|
To change your email, please contact support
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Cancel Button (only in edit mode) */}
|
||
|
|
{editMode && (
|
||
|
|
<div className="flex justify-end pt-4">
|
||
|
|
<button
|
||
|
|
onClick={() => {
|
||
|
|
setEditMode(false);
|
||
|
|
setFormData({
|
||
|
|
first_name: user.first_name || '',
|
||
|
|
last_name: user.last_name || '',
|
||
|
|
username: user.username || '',
|
||
|
|
bio: user.bio || '',
|
||
|
|
favorite_games: user.favorite_games || []
|
||
|
|
});
|
||
|
|
setMessage({ type: '', text: '' });
|
||
|
|
}}
|
||
|
|
className="px-4 py-2 rounded-xl font-medium transition-all duration-200 hover:shadow-md"
|
||
|
|
style={{
|
||
|
|
backgroundColor: 'var(--bg-tertiary)',
|
||
|
|
color: 'var(--text-secondary)',
|
||
|
|
border: '1px solid var(--border)'
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
Cancel
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</Layout>
|
||
|
|
);
|
||
|
|
}
|