- Migrate from React CRA to Next.js 15 with modern architecture - Implement comprehensive shadcn/ui component library - Create complete dashboard system with layouts and navigation - Build authentication pages (login, register) with proper forms - Implement vote management pages (active, upcoming, history, archives) - Add user profile management with security settings - Configure Tailwind CSS with custom dark theme (accent: #e8704b) - Setup TypeScript with strict type checking - Backup old React-based frontend to .backups/frontend-old - All pages compile successfully and build passes linting Pages created: - Home page with hero section and features - Authentication (login/register) - Dashboard with stats and vote cards - Vote management (active, upcoming, history, archives) - User profile with form validation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
277 lines
9.0 KiB
JavaScript
277 lines
9.0 KiB
JavaScript
import React, { useState } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { User, Mail, Lock, LogOut, ArrowLeft, Eye, EyeOff } from 'lucide-react';
|
|
import Alert from '../components/Alert';
|
|
import './ProfilePage.css';
|
|
|
|
export default function ProfilePage({ voter, onLogout }) {
|
|
const navigate = useNavigate();
|
|
const [showPassword, setShowPassword] = useState(false);
|
|
const [showNewPassword, setShowNewPassword] = useState(false);
|
|
const [error, setError] = useState('');
|
|
const [success, setSuccess] = useState('');
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
const [formData, setFormData] = useState({
|
|
nom: voter?.nom || '',
|
|
email: voter?.email || '',
|
|
currentPassword: '',
|
|
newPassword: '',
|
|
confirmPassword: '',
|
|
});
|
|
|
|
const handleChange = (e) => {
|
|
const { name, value } = e.target;
|
|
setFormData(prev => ({ ...prev, [name]: value }));
|
|
};
|
|
|
|
const handleUpdateProfile = async (e) => {
|
|
e.preventDefault();
|
|
setError('');
|
|
setSuccess('');
|
|
setLoading(true);
|
|
|
|
try {
|
|
const token = localStorage.getItem('token');
|
|
const response = await fetch('http://localhost:8000/auth/profile', {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${token}`,
|
|
},
|
|
body: JSON.stringify({
|
|
nom: formData.nom,
|
|
email: formData.email,
|
|
}),
|
|
});
|
|
|
|
if (!response.ok) throw new Error('Erreur lors de la mise à jour');
|
|
|
|
const data = await response.json();
|
|
localStorage.setItem('voter', JSON.stringify(data.voter));
|
|
setSuccess('Profil mis à jour avec succès');
|
|
} catch (err) {
|
|
setError(err.message);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleChangePassword = async (e) => {
|
|
e.preventDefault();
|
|
setError('');
|
|
setSuccess('');
|
|
|
|
if (formData.newPassword !== formData.confirmPassword) {
|
|
setError('Les mots de passe ne correspondent pas');
|
|
return;
|
|
}
|
|
|
|
if (formData.newPassword.length < 8) {
|
|
setError('Le mot de passe doit contenir au moins 8 caractères');
|
|
return;
|
|
}
|
|
|
|
setLoading(true);
|
|
|
|
try {
|
|
const token = localStorage.getItem('token');
|
|
const response = await fetch('http://localhost:8000/auth/change-password', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${token}`,
|
|
},
|
|
body: JSON.stringify({
|
|
current_password: formData.currentPassword,
|
|
new_password: formData.newPassword,
|
|
}),
|
|
});
|
|
|
|
if (!response.ok) throw new Error('Mot de passe actuel incorrect');
|
|
|
|
setSuccess('Mot de passe changé avec succès');
|
|
setFormData(prev => ({
|
|
...prev,
|
|
currentPassword: '',
|
|
newPassword: '',
|
|
confirmPassword: '',
|
|
}));
|
|
} catch (err) {
|
|
setError(err.message);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleLogout = () => {
|
|
localStorage.removeItem('voter');
|
|
localStorage.removeItem('token');
|
|
onLogout();
|
|
navigate('/');
|
|
};
|
|
|
|
return (
|
|
<div className="profile-page">
|
|
<div className="container">
|
|
<button
|
|
onClick={() => navigate('/dashboard')}
|
|
className="back-button"
|
|
>
|
|
<ArrowLeft size={20} />
|
|
Retour au dashboard
|
|
</button>
|
|
|
|
<div className="profile-container">
|
|
<div className="profile-header">
|
|
<div className="profile-avatar">
|
|
{voter?.nom?.charAt(0).toUpperCase()}
|
|
</div>
|
|
<div className="profile-info">
|
|
<h1>{voter?.nom}</h1>
|
|
<p>{voter?.email}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="profile-sections">
|
|
{/* General Information */}
|
|
<div className="profile-section">
|
|
<h2>Informations Générales</h2>
|
|
|
|
{error && (
|
|
<Alert type="error" message={error} onClose={() => setError('')} />
|
|
)}
|
|
{success && (
|
|
<Alert type="success" message={success} onClose={() => setSuccess('')} />
|
|
)}
|
|
|
|
<form onSubmit={handleUpdateProfile} className="profile-form">
|
|
<div className="form-group">
|
|
<label htmlFor="nom">Nom complet</label>
|
|
<div className="input-wrapper">
|
|
<User size={20} className="input-icon" />
|
|
<input
|
|
id="nom"
|
|
type="text"
|
|
name="nom"
|
|
value={formData.nom}
|
|
onChange={handleChange}
|
|
disabled={loading}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="form-group">
|
|
<label htmlFor="email">Email</label>
|
|
<div className="input-wrapper">
|
|
<Mail size={20} className="input-icon" />
|
|
<input
|
|
id="email"
|
|
type="email"
|
|
name="email"
|
|
value={formData.email}
|
|
onChange={handleChange}
|
|
disabled={loading}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<button type="submit" className="btn btn-primary" disabled={loading}>
|
|
{loading ? 'Sauvegarde...' : 'Sauvegarder'}
|
|
</button>
|
|
</form>
|
|
</div>
|
|
|
|
{/* Security Section */}
|
|
<div className="profile-section">
|
|
<h2>Sécurité</h2>
|
|
|
|
<form onSubmit={handleChangePassword} className="profile-form">
|
|
<div className="form-group">
|
|
<label htmlFor="currentPassword">Mot de passe actuel</label>
|
|
<div className="input-wrapper">
|
|
<Lock size={20} className="input-icon" />
|
|
<input
|
|
id="currentPassword"
|
|
type={showPassword ? 'text' : 'password'}
|
|
name="currentPassword"
|
|
placeholder="••••••••"
|
|
value={formData.currentPassword}
|
|
onChange={handleChange}
|
|
disabled={loading}
|
|
/>
|
|
<button
|
|
type="button"
|
|
className="password-toggle"
|
|
onClick={() => setShowPassword(!showPassword)}
|
|
>
|
|
{showPassword ? <EyeOff size={20} /> : <Eye size={20} />}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="form-group">
|
|
<label htmlFor="newPassword">Nouveau mot de passe</label>
|
|
<div className="input-wrapper">
|
|
<Lock size={20} className="input-icon" />
|
|
<input
|
|
id="newPassword"
|
|
type={showNewPassword ? 'text' : 'password'}
|
|
name="newPassword"
|
|
placeholder="Minimum 8 caractères"
|
|
value={formData.newPassword}
|
|
onChange={handleChange}
|
|
disabled={loading}
|
|
/>
|
|
<button
|
|
type="button"
|
|
className="password-toggle"
|
|
onClick={() => setShowNewPassword(!showNewPassword)}
|
|
>
|
|
{showNewPassword ? <EyeOff size={20} /> : <Eye size={20} />}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="form-group">
|
|
<label htmlFor="confirmPassword">Confirmer le mot de passe</label>
|
|
<div className="input-wrapper">
|
|
<Lock size={20} className="input-icon" />
|
|
<input
|
|
id="confirmPassword"
|
|
type="password"
|
|
name="confirmPassword"
|
|
placeholder="••••••••"
|
|
value={formData.confirmPassword}
|
|
onChange={handleChange}
|
|
disabled={loading}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<button type="submit" className="btn btn-primary" disabled={loading}>
|
|
{loading ? 'Mise à jour...' : 'Changer le mot de passe'}
|
|
</button>
|
|
</form>
|
|
</div>
|
|
|
|
{/* Danger Zone */}
|
|
<div className="profile-section danger-zone">
|
|
<h2>Zone Dangereuse</h2>
|
|
<p>Attention: Ces actions sont irréversibles.</p>
|
|
|
|
<button
|
|
onClick={handleLogout}
|
|
className="btn btn-danger"
|
|
>
|
|
<LogOut size={20} />
|
|
Déconnexion
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|