feat: add profile link to header and implement user profile update functionality with email and password management

This commit is contained in:
Julien Froidefond
2025-11-27 13:37:18 +01:00
parent 10ff15392f
commit 873b3dd9f3
7 changed files with 417 additions and 0 deletions

View File

@@ -0,0 +1,115 @@
'use client';
import { useState, useTransition } from 'react';
import { Input, Button } from '@/components/ui';
import { updatePasswordAction } from '@/actions/profile';
export function PasswordForm() {
const [isPending, startTransition] = useTransition();
const [currentPassword, setCurrentPassword] = useState('');
const [newPassword, setNewPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
const canSubmit =
currentPassword.length > 0 &&
newPassword.length >= 6 &&
newPassword === confirmPassword;
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setMessage(null);
if (newPassword !== confirmPassword) {
setMessage({ type: 'error', text: 'Les mots de passe ne correspondent pas' });
return;
}
startTransition(async () => {
const result = await updatePasswordAction({ currentPassword, newPassword });
if (result.success) {
setMessage({ type: 'success', text: 'Mot de passe modifié avec succès' });
setCurrentPassword('');
setNewPassword('');
setConfirmPassword('');
} else {
setMessage({ type: 'error', text: result.error || 'Erreur lors de la modification' });
}
});
}
return (
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label
htmlFor="currentPassword"
className="mb-1.5 block text-sm font-medium text-foreground"
>
Mot de passe actuel
</label>
<Input
id="currentPassword"
type="password"
value={currentPassword}
onChange={(e) => setCurrentPassword(e.target.value)}
required
/>
</div>
<div>
<label
htmlFor="newPassword"
className="mb-1.5 block text-sm font-medium text-foreground"
>
Nouveau mot de passe
</label>
<Input
id="newPassword"
type="password"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
required
minLength={6}
/>
<p className="mt-1 text-xs text-muted">Minimum 6 caractères</p>
</div>
<div>
<label
htmlFor="confirmPassword"
className="mb-1.5 block text-sm font-medium text-foreground"
>
Confirmer le nouveau mot de passe
</label>
<Input
id="confirmPassword"
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
required
/>
{confirmPassword && newPassword !== confirmPassword && (
<p className="mt-1 text-xs text-destructive">
Les mots de passe ne correspondent pas
</p>
)}
</div>
{message && (
<p
className={`text-sm ${
message.type === 'success' ? 'text-success' : 'text-destructive'
}`}
>
{message.text}
</p>
)}
<Button type="submit" disabled={isPending || !canSubmit}>
{isPending ? 'Modification...' : 'Modifier le mot de passe'}
</Button>
</form>
);
}

View File

@@ -0,0 +1,84 @@
'use client';
import { useState, useTransition } from 'react';
import { useRouter } from 'next/navigation';
import { Input, Button } from '@/components/ui';
import { updateProfileAction } from '@/actions/profile';
interface ProfileFormProps {
initialData: {
name: string;
email: string;
};
}
export function ProfileForm({ initialData }: ProfileFormProps) {
const router = useRouter();
const [isPending, startTransition] = useTransition();
const [name, setName] = useState(initialData.name);
const [email, setEmail] = useState(initialData.email);
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
const hasChanges = name !== initialData.name || email !== initialData.email;
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setMessage(null);
startTransition(async () => {
const result = await updateProfileAction({ name, email });
if (result.success) {
setMessage({ type: 'success', text: 'Profil mis à jour avec succès' });
router.refresh();
} else {
setMessage({ type: 'error', text: result.error || 'Erreur lors de la mise à jour' });
}
});
}
return (
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label htmlFor="name" className="mb-1.5 block text-sm font-medium text-foreground">
Nom
</label>
<Input
id="name"
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Votre nom"
/>
</div>
<div>
<label htmlFor="email" className="mb-1.5 block text-sm font-medium text-foreground">
Email
</label>
<Input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
</div>
{message && (
<p
className={`text-sm ${
message.type === 'success' ? 'text-success' : 'text-destructive'
}`}
>
{message.text}
</p>
)}
<Button type="submit" disabled={isPending || !hasChanges}>
{isPending ? 'Enregistrement...' : 'Enregistrer les modifications'}
</Button>
</form>
);
}

75
src/app/profile/page.tsx Normal file
View File

@@ -0,0 +1,75 @@
import { auth } from '@/lib/auth';
import { redirect } from 'next/navigation';
import { getUserById } from '@/services/auth';
import { ProfileForm } from './ProfileForm';
import { PasswordForm } from './PasswordForm';
export default async function ProfilePage() {
const session = await auth();
if (!session?.user?.id) {
redirect('/login');
}
const user = await getUserById(session.user.id);
if (!user) {
redirect('/login');
}
return (
<main className="mx-auto max-w-2xl px-4 py-8">
<div className="mb-8">
<h1 className="text-3xl font-bold text-foreground">Mon Profil</h1>
<p className="mt-1 text-muted">Gérez vos informations personnelles</p>
</div>
<div className="space-y-8">
{/* Profile Info */}
<section className="rounded-xl border border-border bg-card p-6">
<h2 className="mb-6 text-xl font-semibold text-foreground">
Informations personnelles
</h2>
<ProfileForm
initialData={{
name: user.name || '',
email: user.email,
}}
/>
</section>
{/* Password */}
<section className="rounded-xl border border-border bg-card p-6">
<h2 className="mb-6 text-xl font-semibold text-foreground">
Changer le mot de passe
</h2>
<PasswordForm />
</section>
{/* Account Info */}
<section className="rounded-xl border border-border bg-card p-6">
<h2 className="mb-4 text-xl font-semibold text-foreground">
Informations du compte
</h2>
<div className="space-y-3 text-sm">
<div className="flex justify-between">
<span className="text-muted">ID du compte</span>
<span className="font-mono text-foreground">{user.id}</span>
</div>
<div className="flex justify-between">
<span className="text-muted">Membre depuis</span>
<span className="text-foreground">
{new Date(user.createdAt).toLocaleDateString('fr-FR', {
day: 'numeric',
month: 'long',
year: 'numeric',
})}
</span>
</div>
</div>
</section>
</div>
</main>
);
}