diff --git a/pages/api/user/password.js b/pages/api/user/password.js new file mode 100644 index 0000000..a4c64b0 --- /dev/null +++ b/pages/api/user/password.js @@ -0,0 +1,75 @@ +import { sql } from '@vercel/postgres'; +import bcrypt from 'bcryptjs'; +import { getUserFromRequest } from '../../../lib/permission-middleware'; + +export default async function handler(req, res) { + // Set CORS headers + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'PUT, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); + + // Handle preflight requests + if (req.method === 'OPTIONS') { + res.status(200).end(); + return; + } + + if (req.method !== 'PUT') { + return res.status(405).json({ error: 'Method not allowed' }); + } + + try { + // Get authenticated user + const user = await getUserFromRequest(req); + if (!user) { + return res.status(401).json({ error: 'Authentication required' }); + } + + const { current_password, new_password } = req.body; + + // Validate input + if (!current_password || !new_password) { + return res.status(400).json({ error: 'Current password and new password are required' }); + } + + if (new_password.length < 8) { + return res.status(400).json({ error: 'New password must be at least 8 characters long' }); + } + + // Get current user password + const userResult = await sql` + SELECT password FROM users WHERE id = ${user.userId} + `; + + if (userResult.rows.length === 0) { + return res.status(404).json({ error: 'User not found' }); + } + + const currentHashedPassword = userResult.rows[0].password; + + // Verify current password + const isCurrentPasswordValid = await bcrypt.compare(current_password, currentHashedPassword); + if (!isCurrentPasswordValid) { + return res.status(400).json({ error: 'Current password is incorrect' }); + } + + // Hash new password + const saltRounds = 12; + const newHashedPassword = await bcrypt.hash(new_password, saltRounds); + + // Update password + await sql` + UPDATE users + SET + password = ${newHashedPassword}, + updated_at = CURRENT_TIMESTAMP + WHERE id = ${user.userId} + `; + + res.status(200).json({ message: 'Password updated successfully' }); + + } catch (error) { + console.error('Password change error:', error); + res.status(500).json({ error: 'Internal server error' }); + } +} \ No newline at end of file diff --git a/pages/api/user/profile.js b/pages/api/user/profile.js new file mode 100644 index 0000000..d519f4e --- /dev/null +++ b/pages/api/user/profile.js @@ -0,0 +1,124 @@ +import { sql } from '@vercel/postgres'; +import { getUserFromRequest } from '../../../lib/permission-middleware'; + +export default async function handler(req, res) { + // Set CORS headers + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'GET, PUT, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); + + // Handle preflight requests + if (req.method === 'OPTIONS') { + res.status(200).end(); + return; + } + + try { + // Get authenticated user + const user = await getUserFromRequest(req); + if (!user) { + return res.status(401).json({ error: 'Authentication required' }); + } + + if (req.method === 'GET') { + // Get user profile + const result = await sql` + SELECT + id, email, role, first_name, last_name, username, bio, avatar_url, + favorite_games, collection_visibility, preferred_currency, + cards_per_page, default_view, notifications_email, + notifications_marketing, two_factor_enabled, theme, language, + created_at, updated_at + FROM users + WHERE id = ${user.userId} + `; + + if (result.rows.length === 0) { + return res.status(404).json({ error: 'User not found' }); + } + + const userProfile = result.rows[0]; + + // Parse JSON fields + if (userProfile.favorite_games && typeof userProfile.favorite_games === 'string') { + try { + userProfile.favorite_games = JSON.parse(userProfile.favorite_games); + } catch (e) { + userProfile.favorite_games = ['MTG']; + } + } + + res.status(200).json(userProfile); + + } else if (req.method === 'PUT') { + // Update user profile + const { + first_name, + last_name, + username, + bio, + favorite_games + } = req.body; + + // Validate username uniqueness if provided + if (username) { + const existingUser = await sql` + SELECT id FROM users + WHERE username = ${username} AND id != ${user.userId} + `; + + if (existingUser.rows.length > 0) { + return res.status(400).json({ error: 'Username already taken' }); + } + } + + // Validate favorite_games format + if (favorite_games && !Array.isArray(favorite_games)) { + return res.status(400).json({ error: 'favorite_games must be an array' }); + } + + // Update user profile + const result = await sql` + UPDATE users + SET + first_name = ${first_name || null}, + last_name = ${last_name || null}, + username = ${username || null}, + bio = ${bio || null}, + favorite_games = ${favorite_games ? JSON.stringify(favorite_games) : null}, + updated_at = CURRENT_TIMESTAMP + WHERE id = ${user.userId} + RETURNING + id, email, role, first_name, last_name, username, bio, avatar_url, + favorite_games, collection_visibility, preferred_currency, + cards_per_page, default_view, notifications_email, + notifications_marketing, two_factor_enabled, theme, language, + created_at, updated_at + `; + + if (result.rows.length === 0) { + return res.status(404).json({ error: 'User not found' }); + } + + const updatedProfile = result.rows[0]; + + // Parse JSON fields + if (updatedProfile.favorite_games && typeof updatedProfile.favorite_games === 'string') { + try { + updatedProfile.favorite_games = JSON.parse(updatedProfile.favorite_games); + } catch (e) { + updatedProfile.favorite_games = ['MTG']; + } + } + + res.status(200).json(updatedProfile); + + } else { + res.status(405).json({ error: 'Method not allowed' }); + } + + } catch (error) { + console.error('Profile API error:', error); + res.status(500).json({ error: 'Internal server error' }); + } +} \ No newline at end of file diff --git a/pages/api/user/settings.js b/pages/api/user/settings.js new file mode 100644 index 0000000..40683d4 --- /dev/null +++ b/pages/api/user/settings.js @@ -0,0 +1,229 @@ +import { sql } from '@vercel/postgres'; +import { getUserFromRequest } from '../../../lib/permission-middleware'; + +export default async function handler(req, res) { + // Set CORS headers + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'GET, PUT, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); + + // Handle preflight requests + if (req.method === 'OPTIONS') { + res.status(200).end(); + return; + } + + try { + // Get authenticated user + const user = await getUserFromRequest(req); + if (!user) { + return res.status(401).json({ error: 'Authentication required' }); + } + + if (req.method === 'GET') { + // Get user and settings + const result = await sql` + SELECT + id, email, role, first_name, last_name, username, + collection_visibility, preferred_currency, cards_per_page, + default_view, notifications_email, notifications_marketing, + two_factor_enabled, theme, language, + created_at, updated_at + FROM users + WHERE id = ${user.userId} + `; + + if (result.rows.length === 0) { + return res.status(404).json({ error: 'User not found' }); + } + + const userData = result.rows[0]; + + // Structure response + const response = { + user: { + id: userData.id, + email: userData.email, + role: userData.role, + first_name: userData.first_name, + last_name: userData.last_name, + username: userData.username + }, + settings: { + // Account Settings + collection_visibility: userData.collection_visibility || 'private', + preferred_currency: userData.preferred_currency || 'USD', + cards_per_page: userData.cards_per_page || 50, + default_view: userData.default_view || 'grid', + + // Security Settings + two_factor_enabled: userData.two_factor_enabled || false, + + // Notification Settings + notifications_email: userData.notifications_email !== false, // Default to true + notifications_marketing: userData.notifications_marketing || false, + + // Display Settings + theme: userData.theme || 'system', + language: userData.language || 'en' + } + }; + + res.status(200).json(response); + + } else if (req.method === 'PUT') { + // Update user settings + const { + collection_visibility, + preferred_currency, + cards_per_page, + default_view, + two_factor_enabled, + notifications_email, + notifications_marketing, + theme, + language + } = req.body; + + // Validate enum values + const validVisibility = ['private', 'public', 'unlisted']; + const validCurrency = ['USD', 'EUR', 'GBP', 'CAD', 'JPY']; + const validCardsPerPage = [25, 50, 100]; + const validView = ['grid', 'list']; + const validTheme = ['light', 'dark', 'system']; + const validLanguage = ['en', 'es', 'fr', 'de', 'ja']; + + // Validate inputs + if (collection_visibility && !validVisibility.includes(collection_visibility)) { + return res.status(400).json({ error: 'Invalid collection visibility' }); + } + if (preferred_currency && !validCurrency.includes(preferred_currency)) { + return res.status(400).json({ error: 'Invalid preferred currency' }); + } + if (cards_per_page && !validCardsPerPage.includes(cards_per_page)) { + return res.status(400).json({ error: 'Invalid cards per page value' }); + } + if (default_view && !validView.includes(default_view)) { + return res.status(400).json({ error: 'Invalid default view' }); + } + if (theme && !validTheme.includes(theme)) { + return res.status(400).json({ error: 'Invalid theme' }); + } + if (language && !validLanguage.includes(language)) { + return res.status(400).json({ error: 'Invalid language' }); + } + + // Build update query dynamically + const updateFields = []; + const updateValues = []; + let paramIndex = 1; + + if (collection_visibility !== undefined) { + updateFields.push(`collection_visibility = $${paramIndex}`); + updateValues.push(collection_visibility); + paramIndex++; + } + if (preferred_currency !== undefined) { + updateFields.push(`preferred_currency = $${paramIndex}`); + updateValues.push(preferred_currency); + paramIndex++; + } + if (cards_per_page !== undefined) { + updateFields.push(`cards_per_page = $${paramIndex}`); + updateValues.push(cards_per_page); + paramIndex++; + } + if (default_view !== undefined) { + updateFields.push(`default_view = $${paramIndex}`); + updateValues.push(default_view); + paramIndex++; + } + if (two_factor_enabled !== undefined) { + updateFields.push(`two_factor_enabled = $${paramIndex}`); + updateValues.push(two_factor_enabled); + paramIndex++; + } + if (notifications_email !== undefined) { + updateFields.push(`notifications_email = $${paramIndex}`); + updateValues.push(notifications_email); + paramIndex++; + } + if (notifications_marketing !== undefined) { + updateFields.push(`notifications_marketing = $${paramIndex}`); + updateValues.push(notifications_marketing); + paramIndex++; + } + if (theme !== undefined) { + updateFields.push(`theme = $${paramIndex}`); + updateValues.push(theme); + paramIndex++; + } + if (language !== undefined) { + updateFields.push(`language = $${paramIndex}`); + updateValues.push(language); + paramIndex++; + } + + if (updateFields.length === 0) { + return res.status(400).json({ error: 'No settings to update' }); + } + + // Add updated_at and user_id + updateFields.push('updated_at = CURRENT_TIMESTAMP'); + updateValues.push(user.userId); + + // Execute update + const updateQuery = ` + UPDATE users + SET ${updateFields.join(', ')} + WHERE id = $${paramIndex} + RETURNING + id, email, role, first_name, last_name, username, + collection_visibility, preferred_currency, cards_per_page, + default_view, notifications_email, notifications_marketing, + two_factor_enabled, theme, language, + created_at, updated_at + `; + + const result = await sql.query(updateQuery, updateValues); + + if (result.rows.length === 0) { + return res.status(404).json({ error: 'User not found' }); + } + + const userData = result.rows[0]; + + // Structure response + const response = { + user: { + id: userData.id, + email: userData.email, + role: userData.role, + first_name: userData.first_name, + last_name: userData.last_name, + username: userData.username + }, + settings: { + collection_visibility: userData.collection_visibility, + preferred_currency: userData.preferred_currency, + cards_per_page: userData.cards_per_page, + default_view: userData.default_view, + two_factor_enabled: userData.two_factor_enabled, + notifications_email: userData.notifications_email, + notifications_marketing: userData.notifications_marketing, + theme: userData.theme, + language: userData.language + } + }; + + res.status(200).json(response); + + } else { + res.status(405).json({ error: 'Method not allowed' }); + } + + } catch (error) { + console.error('Settings API error:', error); + res.status(500).json({ error: 'Internal server error' }); + } +} \ No newline at end of file diff --git a/pages/api/user/stats.js b/pages/api/user/stats.js new file mode 100644 index 0000000..b490c76 --- /dev/null +++ b/pages/api/user/stats.js @@ -0,0 +1,136 @@ +import { sql } from '@vercel/postgres'; +import { getUserFromRequest } from '../../../lib/permission-middleware'; + +export default async function handler(req, res) { + // Set CORS headers + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); + + // Handle preflight requests + if (req.method === 'OPTIONS') { + res.status(200).end(); + return; + } + + if (req.method !== 'GET') { + return res.status(405).json({ error: 'Method not allowed' }); + } + + try { + // Get authenticated user + const user = await getUserFromRequest(req); + if (!user) { + return res.status(401).json({ error: 'Authentication required' }); + } + + // Get total cards owned by user + const cardsResult = await sql` + SELECT COALESCE(SUM(quantity), 0) as total_cards + FROM user_cards + WHERE user_id = ${user.userId} + `; + + // Get total collections owned by user + const collectionsResult = await sql` + SELECT COUNT(*) as total_collections + FROM collections + WHERE user_id = ${user.userId} + `; + + // Get total decks owned by user + const decksResult = await sql` + SELECT COUNT(*) as total_decks + FROM decks + WHERE user_id = ${user.userId} + `; + + // Get total value of user's cards + const valueResult = await sql` + SELECT COALESCE(SUM(cards.market_price * user_cards.quantity), 0) as total_value + FROM user_cards + JOIN cards ON user_cards.card_id = cards.id + WHERE user_cards.user_id = ${user.userId} + AND cards.market_price IS NOT NULL + `; + + // Get game breakdown + const gameBreakdownResult = await sql` + SELECT + cards.game, + COUNT(DISTINCT cards.id) as unique_cards, + COALESCE(SUM(user_cards.quantity), 0) as total_quantity, + COALESCE(SUM(cards.market_price * user_cards.quantity), 0) as total_value + FROM user_cards + JOIN cards ON user_cards.card_id = cards.id + WHERE user_cards.user_id = ${user.userId} + GROUP BY cards.game + ORDER BY total_value DESC + `; + + // Get rarity breakdown + const rarityBreakdownResult = await sql` + SELECT + cards.rarity, + COUNT(DISTINCT cards.id) as unique_cards, + COALESCE(SUM(user_cards.quantity), 0) as total_quantity, + COALESCE(SUM(cards.market_price * user_cards.quantity), 0) as total_value + FROM user_cards + JOIN cards ON user_cards.card_id = cards.id + WHERE user_cards.user_id = ${user.userId} + GROUP BY cards.rarity + ORDER BY total_value DESC + `; + + // Get recent activity (last 10 cards added) + const recentActivityResult = await sql` + SELECT + cards.name, + cards.game, + cards.rarity, + cards.image_url, + cards.market_price, + user_cards.quantity, + user_cards.created_at + FROM user_cards + JOIN cards ON user_cards.card_id = cards.id + WHERE user_cards.user_id = ${user.userId} + ORDER BY user_cards.created_at DESC + LIMIT 10 + `; + + const stats = { + totalCards: parseInt(cardsResult.rows[0].total_cards) || 0, + totalCollections: parseInt(collectionsResult.rows[0].total_collections) || 0, + totalDecks: parseInt(decksResult.rows[0].total_decks) || 0, + totalValue: parseFloat(valueResult.rows[0].total_value) || 0, + gameBreakdown: gameBreakdownResult.rows.map(row => ({ + game: row.game, + uniqueCards: parseInt(row.unique_cards), + totalQuantity: parseInt(row.total_quantity), + totalValue: parseFloat(row.total_value) + })), + rarityBreakdown: rarityBreakdownResult.rows.map(row => ({ + rarity: row.rarity, + uniqueCards: parseInt(row.unique_cards), + totalQuantity: parseInt(row.total_quantity), + totalValue: parseFloat(row.total_value) + })), + recentActivity: recentActivityResult.rows.map(row => ({ + name: row.name, + game: row.game, + rarity: row.rarity, + imageUrl: row.image_url, + marketPrice: parseFloat(row.market_price) || 0, + quantity: parseInt(row.quantity), + addedAt: row.created_at + })) + }; + + res.status(200).json(stats); + + } catch (error) { + console.error('Stats API error:', error); + res.status(500).json({ error: 'Internal server error' }); + } +} \ No newline at end of file diff --git a/pages/profile.js b/pages/profile.js new file mode 100644 index 0000000..54f15f6 --- /dev/null +++ b/pages/profile.js @@ -0,0 +1,625 @@ +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 ( + +
+
+
+
+ ); + } + + return ( + +
+ {/* Header */} +
+

+ Profile +

+

+ Manage your account information and preferences +

+
+ + {/* Message */} + {message.text && ( +
+ {message.text} +
+ )} + +
+ {/* Profile Card */} +
+
+ {/* Avatar */} +
+
+ {user.avatar_url ? ( + Profile + ) : ( +
+ {getInitials()} +
+ )} + + {/* Avatar Upload Button */} + + + +
+ + {/* Generate Avatar Button */} + +
+ + {/* Basic Info */} +
+

+ {getDisplayName()} +

+ {user.username && ( +

+ @{user.username} +

+ )} +

+ {user.email} +

+
+ + {user.role?.toUpperCase()} + +
+
+ + {/* Bio */} + {user.bio && ( +
+

+ {user.bio} +

+
+ )} + + {/* Member Since */} +
+

+ Member since {formatDate(user.created_at)} +

+
+
+ + {/* Stats Card */} +
+

+ Collection Stats +

+
+
+ Total Cards + + {stats.totalCards.toLocaleString()} + +
+
+ Collections + + {stats.totalCollections} + +
+
+ Decks + + {stats.totalDecks} + +
+
+ Total Value + + {formatCurrency(stats.totalValue)} + +
+
+
+
+ + {/* Profile Form */} +
+
+
+

+ Profile Information +

+ +
+ +
+ {/* Name Fields */} +
+
+ + {editMode ? ( + handleInputChange('first_name', e.target.value)} + className="input-field w-full" + placeholder="Enter your first name" + /> + ) : ( +

+ {user.first_name || 'Not set'} +

+ )} +
+
+ + {editMode ? ( + handleInputChange('last_name', e.target.value)} + className="input-field w-full" + placeholder="Enter your last name" + /> + ) : ( +

+ {user.last_name || 'Not set'} +

+ )} +
+
+ + {/* Username */} +
+ + {editMode ? ( + handleInputChange('username', e.target.value)} + className="input-field w-full" + placeholder="Choose a unique username" + /> + ) : ( +

+ {user.username || 'Not set'} +

+ )} +
+ + {/* Bio */} +
+ + {editMode ? ( +