Replace placeholder implementation with proper API integration: - Create /api/user/profile PUT endpoint with JWT validation - Update profile page to call actual API instead of setTimeout - Use refreshUser() to update UI immediately after changes - Ensure name changes persist to database and across page refreshes 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
241 lines
7.2 KiB
TypeScript
241 lines
7.2 KiB
TypeScript
'use client'
|
|
|
|
import { useState } from 'react'
|
|
import { useTranslations } from 'next-intl'
|
|
import { useAuth } from '@/hooks/use-auth'
|
|
import { ProtectedRoute } from '@/components/auth/protected-route'
|
|
import {
|
|
Container,
|
|
Paper,
|
|
Box,
|
|
Typography,
|
|
TextField,
|
|
Button,
|
|
Avatar,
|
|
Grid,
|
|
Card,
|
|
CardContent,
|
|
Divider,
|
|
Alert,
|
|
CircularProgress
|
|
} from '@mui/material'
|
|
import {
|
|
Person,
|
|
Email,
|
|
AdminPanelSettings,
|
|
Save,
|
|
Edit
|
|
} from '@mui/icons-material'
|
|
|
|
export default function ProfilePage() {
|
|
const { user, refreshUser } = useAuth()
|
|
const t = useTranslations('profile')
|
|
const [isEditing, setIsEditing] = useState(false)
|
|
const [name, setName] = useState(user?.name || '')
|
|
const [loading, setLoading] = useState(false)
|
|
const [message, setMessage] = useState('')
|
|
|
|
const handleSave = async () => {
|
|
setLoading(true)
|
|
setMessage('')
|
|
|
|
try {
|
|
const token = localStorage.getItem('authToken')
|
|
if (!token) {
|
|
setMessage(t('updateError'))
|
|
return
|
|
}
|
|
|
|
const response = await fetch(`/api/user/profile?locale=${navigator.language.split('-')[0]}`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${token}`
|
|
},
|
|
body: JSON.stringify({ name })
|
|
})
|
|
|
|
const data = await response.json()
|
|
|
|
if (response.ok) {
|
|
setMessage(t('profileUpdated'))
|
|
setIsEditing(false)
|
|
// Refresh user data in context to show updated name
|
|
await refreshUser()
|
|
} else {
|
|
setMessage(data.error || t('updateError'))
|
|
}
|
|
} catch (error) {
|
|
setMessage(t('updateError'))
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
const handleCancel = () => {
|
|
setName(user?.name || '')
|
|
setIsEditing(false)
|
|
}
|
|
|
|
const getRoleTranslation = (role: string) => {
|
|
switch (role) {
|
|
case 'admin':
|
|
return t('admin')
|
|
case 'moderator':
|
|
return t('moderator')
|
|
default:
|
|
return t('user')
|
|
}
|
|
}
|
|
|
|
return (
|
|
<ProtectedRoute>
|
|
<Container maxWidth="md" sx={{ py: 4 }}>
|
|
<Paper elevation={3} sx={{ p: 4 }}>
|
|
{/* Header */}
|
|
<Box textAlign="center" mb={4}>
|
|
<Avatar
|
|
sx={{
|
|
width: 100,
|
|
height: 100,
|
|
fontSize: 40,
|
|
mx: 'auto',
|
|
mb: 2,
|
|
bgcolor: 'primary.main'
|
|
}}
|
|
>
|
|
{user?.name ? user.name.charAt(0).toUpperCase() : <Person />}
|
|
</Avatar>
|
|
<Typography variant="h4" component="h1" gutterBottom>
|
|
{t('title')}
|
|
</Typography>
|
|
<Typography variant="body1" color="text.secondary">
|
|
{t('subtitle')}
|
|
</Typography>
|
|
</Box>
|
|
|
|
<Divider sx={{ mb: 4 }} />
|
|
|
|
{/* Profile Information */}
|
|
<Grid container spacing={3}>
|
|
{/* Personal Information Card */}
|
|
<Grid item xs={12} md={8}>
|
|
<Card variant="outlined">
|
|
<CardContent>
|
|
<Box display="flex" justifyContent="space-between" alignItems="center" mb={3}>
|
|
<Typography variant="h6" component="h2">
|
|
{t('personalInfo')}
|
|
</Typography>
|
|
{!isEditing && (
|
|
<Button
|
|
startIcon={<Edit />}
|
|
onClick={() => setIsEditing(true)}
|
|
variant="outlined"
|
|
size="small"
|
|
>
|
|
{t('edit')}
|
|
</Button>
|
|
)}
|
|
</Box>
|
|
|
|
<Box sx={{ mb: 3 }}>
|
|
<TextField
|
|
fullWidth
|
|
label={t('name')}
|
|
value={name}
|
|
onChange={(e) => setName(e.target.value)}
|
|
disabled={!isEditing || loading}
|
|
variant="outlined"
|
|
margin="normal"
|
|
InputProps={{
|
|
startAdornment: <Person sx={{ mr: 1, color: 'action.active' }} />
|
|
}}
|
|
/>
|
|
</Box>
|
|
|
|
<Box sx={{ mb: 3 }}>
|
|
<TextField
|
|
fullWidth
|
|
label={t('email')}
|
|
value={user?.email || ''}
|
|
disabled
|
|
variant="outlined"
|
|
margin="normal"
|
|
InputProps={{
|
|
startAdornment: <Email sx={{ mr: 1, color: 'action.active' }} />
|
|
}}
|
|
helperText={t('emailCannotChange')}
|
|
/>
|
|
</Box>
|
|
|
|
{isEditing && (
|
|
<Box display="flex" gap={2} mt={3}>
|
|
<Button
|
|
variant="contained"
|
|
startIcon={loading ? <CircularProgress size={20} /> : <Save />}
|
|
onClick={handleSave}
|
|
disabled={loading}
|
|
>
|
|
{loading ? t('saving') : t('save')}
|
|
</Button>
|
|
<Button
|
|
variant="outlined"
|
|
onClick={handleCancel}
|
|
disabled={loading}
|
|
>
|
|
{t('cancel')}
|
|
</Button>
|
|
</Box>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</Grid>
|
|
|
|
{/* Account Details Card */}
|
|
<Grid item xs={12} md={4}>
|
|
<Card variant="outlined">
|
|
<CardContent>
|
|
<Typography variant="h6" component="h2" gutterBottom>
|
|
{t('accountDetails')}
|
|
</Typography>
|
|
|
|
<Box display="flex" alignItems="center" mb={2}>
|
|
<AdminPanelSettings sx={{ mr: 1, color: 'action.active' }} />
|
|
<Box>
|
|
<Typography variant="body2" color="text.secondary">
|
|
{t('role')}
|
|
</Typography>
|
|
<Typography variant="body1">
|
|
{getRoleTranslation(user?.role || 'user')}
|
|
</Typography>
|
|
</Box>
|
|
</Box>
|
|
|
|
<Box>
|
|
<Typography variant="body2" color="text.secondary">
|
|
{t('memberSince')}
|
|
</Typography>
|
|
<Typography variant="body1">
|
|
{new Date().toLocaleDateString()} {/* TODO: Use actual creation date */}
|
|
</Typography>
|
|
</Box>
|
|
</CardContent>
|
|
</Card>
|
|
</Grid>
|
|
</Grid>
|
|
|
|
{/* Success/Error Message */}
|
|
{message && (
|
|
<Alert
|
|
severity={message.includes(t('updateError')) ? 'error' : 'success'}
|
|
sx={{ mt: 3 }}
|
|
onClose={() => setMessage('')}
|
|
>
|
|
{message}
|
|
</Alert>
|
|
)}
|
|
</Paper>
|
|
</Container>
|
|
</ProtectedRoute>
|
|
)
|
|
} |