freedge/frontend/src/pages/Profile.tsx
2025-03-10 00:24:26 +01:00

551 lines
20 KiB
TypeScript

import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Separator } from "@/components/ui/separator";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { AlertCircle, Save, User, Lock, LogOut, Trash2 } from "lucide-react";
import { apiService } from "@/api/base";
import { recipeService, Recipe } from "@/api/recipe";
// Types pour les données utilisateur
interface User {
id: string;
email: string;
username?: string;
firstName?: string;
lastName?: string;
bio?: string;
avatarUrl?: string;
createdAt: string;
}
// Service utilisateur
const userService = {
getCurrentUser: async (): Promise<User> => {
return apiService.get<User>('users/me');
},
updateProfile: async (data: Partial<User>): Promise<User> => {
return apiService.put<User>('users/me', data);
},
changePassword: async (data: { currentPassword: string; newPassword: string }): Promise<void> => {
return apiService.post('users/change-password', data);
},
deleteAccount: async (): Promise<void> => {
return apiService.delete('users/me');
},
logout: async (): Promise<void> => {
return apiService.post('auth/logout', {});
}
};
export default function Profile() {
const navigate = useNavigate();
const [user, setUser] = useState<User | null>(null);
const [userRecipes, setUserRecipes] = useState<Recipe[]>([]);
const [favoriteRecipes, setFavoriteRecipes] = useState<Recipe[]>([]);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [error, setError] = useState("");
const [success, setSuccess] = useState("");
// États pour le formulaire de profil
const [profileForm, setProfileForm] = useState({
username: "",
firstName: "",
lastName: "",
bio: ""
});
// États pour le formulaire de mot de passe
const [passwordForm, setPasswordForm] = useState({
currentPassword: "",
newPassword: "",
confirmPassword: ""
});
useEffect(() => {
const fetchUserData = async () => {
try {
setLoading(true);
// Récupérer les données de l'utilisateur
const userData = await userService.getCurrentUser();
setUser(userData);
setProfileForm({
username: userData.username || "",
firstName: userData.firstName || "",
lastName: userData.lastName || "",
bio: userData.bio || ""
});
// Récupérer les recettes de l'utilisateur
const recipes = await recipeService.getUserRecipes();
setUserRecipes(recipes);
// Récupérer les recettes favorites
const favorites = await recipeService.getFavoriteRecipes();
setFavoriteRecipes(favorites);
} catch (err) {
console.error("Erreur lors du chargement du profil:", err);
setError("Impossible de charger les données du profil");
// Rediriger vers la page de connexion si non authentifié
if (err instanceof Error && err.message.includes("401")) {
localStorage.removeItem("token");
navigate("/auth/login");
}
} finally {
setLoading(false);
}
};
fetchUserData();
}, [navigate]);
const handleProfileChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target;
setProfileForm(prev => ({ ...prev, [name]: value }));
};
const handlePasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setPasswordForm(prev => ({ ...prev, [name]: value }));
};
const handleProfileSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError("");
setSuccess("");
try {
setSaving(true);
const updatedUser = await userService.updateProfile(profileForm);
setUser(updatedUser);
setSuccess("Profil mis à jour avec succès");
} catch (err) {
console.error("Erreur lors de la mise à jour du profil:", err);
setError("Impossible de mettre à jour le profil");
} finally {
setSaving(false);
}
};
const handlePasswordSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError("");
setSuccess("");
// Vérifier que les mots de passe correspondent
if (passwordForm.newPassword !== passwordForm.confirmPassword) {
setError("Les mots de passe ne correspondent pas");
return;
}
try {
setSaving(true);
await userService.changePassword({
currentPassword: passwordForm.currentPassword,
newPassword: passwordForm.newPassword
});
setSuccess("Mot de passe modifié avec succès");
setPasswordForm({
currentPassword: "",
newPassword: "",
confirmPassword: ""
});
} catch (err) {
console.error("Erreur lors du changement de mot de passe:", err);
setError("Impossible de changer le mot de passe");
} finally {
setSaving(false);
}
};
const handleLogout = async () => {
try {
await userService.logout();
} catch (err) {
console.error("Erreur lors de la déconnexion:", err);
} finally {
localStorage.removeItem("token");
navigate("/auth/login");
}
};
const handleDeleteAccount = async () => {
if (!window.confirm("Êtes-vous sûr de vouloir supprimer votre compte ? Cette action est irréversible.")) {
return;
}
try {
await userService.deleteAccount();
localStorage.removeItem("token");
navigate("/auth/login");
} catch (err) {
console.error("Erreur lors de la suppression du compte:", err);
setError("Impossible de supprimer le compte");
}
};
if (loading) {
return (
<div className="flex justify-center py-12">
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent"></div>
</div>
);
}
if (!user) {
return (
<div className="rounded-md bg-red-50 p-6 text-center text-red-700 dark:bg-red-900/50 dark:text-red-200">
<h2 className="text-xl font-bold">Erreur</h2>
<p>Utilisateur non trouvé ou non connecté</p>
<Button
variant="outline"
className="mt-4"
onClick={() => navigate("/auth/login")}
>
Se connecter
</Button>
</div>
);
}
return (
<div className="space-y-8">
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div>
<h1 className="text-3xl font-bold tracking-tight">Profil</h1>
<p className="text-muted-foreground">
Gérez vos informations personnelles et vos préférences
</p>
</div>
<Button variant="destructive" onClick={handleLogout}>
<LogOut className="mr-2 h-4 w-4" />
Déconnexion
</Button>
</div>
{error && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Erreur</AlertTitle>
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
{success && (
<Alert className="bg-green-50 text-green-700 dark:bg-green-900/50 dark:text-green-200">
<AlertTitle>Succès</AlertTitle>
<AlertDescription>{success}</AlertDescription>
</Alert>
)}
<div className="flex flex-col gap-8 md:flex-row">
<div className="md:w-1/3">
<Card>
<CardHeader>
<CardTitle>Informations</CardTitle>
<CardDescription>Vos informations de compte</CardDescription>
</CardHeader>
<CardContent className="flex flex-col items-center space-y-4">
<Avatar className="h-24 w-24">
<AvatarImage src={user.avatarUrl} alt={user.username || user.email} />
<AvatarFallback className="text-2xl">
{user.firstName?.[0]}{user.lastName?.[0] || user.email[0].toUpperCase()}
</AvatarFallback>
</Avatar>
<div className="text-center">
<h3 className="text-xl font-bold">{user.username || "Utilisateur"}</h3>
<p className="text-sm text-muted-foreground">{user.email}</p>
{(user.firstName || user.lastName) && (
<p className="text-sm">{user.firstName} {user.lastName}</p>
)}
</div>
<div className="w-full">
<p className="text-sm text-muted-foreground">Membre depuis</p>
<p>{new Date(user.createdAt).toLocaleDateString()}</p>
</div>
{user.bio && (
<div className="w-full">
<p className="text-sm text-muted-foreground">Bio</p>
<p className="text-sm">{user.bio}</p>
</div>
)}
</CardContent>
<CardFooter>
<Button
variant="destructive"
className="w-full"
onClick={handleDeleteAccount}
>
<Trash2 className="mr-2 h-4 w-4" />
Supprimer mon compte
</Button>
</CardFooter>
</Card>
</div>
<div className="flex-1">
<Tabs defaultValue="profile">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="profile">
<User className="mr-2 h-4 w-4" />
Profil
</TabsTrigger>
<TabsTrigger value="security">
<Lock className="mr-2 h-4 w-4" />
Sécurité
</TabsTrigger>
</TabsList>
<TabsContent value="profile" className="mt-6">
<Card>
<CardHeader>
<CardTitle>Informations du profil</CardTitle>
<CardDescription>
Mettez à jour vos informations personnelles
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleProfileSubmit} className="space-y-4">
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="firstName">Prénom</Label>
<Input
id="firstName"
name="firstName"
value={profileForm.firstName}
onChange={handleProfileChange}
/>
</div>
<div className="space-y-2">
<Label htmlFor="lastName">Nom</Label>
<Input
id="lastName"
name="lastName"
value={profileForm.lastName}
onChange={handleProfileChange}
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="username">Nom d'utilisateur</Label>
<Input
id="username"
name="username"
value={profileForm.username}
onChange={handleProfileChange}
/>
</div>
<div className="space-y-2">
<Label htmlFor="bio">Bio</Label>
<textarea
id="bio"
name="bio"
value={profileForm.bio}
onChange={handleProfileChange}
className="w-full min-h-[100px] rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
/>
</div>
<Button type="submit" disabled={saving}>
{saving ? (
<>
<div className="mr-2 h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent"></div>
Enregistrement...
</>
) : (
<>
<Save className="mr-2 h-4 w-4" />
Enregistrer les modifications
</>
)}
</Button>
</form>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="security" className="mt-6">
<Card>
<CardHeader>
<CardTitle>Sécurité du compte</CardTitle>
<CardDescription>
Mettez à jour votre mot de passe
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handlePasswordSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="currentPassword">Mot de passe actuel</Label>
<Input
id="currentPassword"
name="currentPassword"
type="password"
value={passwordForm.currentPassword}
onChange={handlePasswordChange}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="newPassword">Nouveau mot de passe</Label>
<Input
id="newPassword"
name="newPassword"
type="password"
value={passwordForm.newPassword}
onChange={handlePasswordChange}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="confirmPassword">Confirmer le mot de passe</Label>
<Input
id="confirmPassword"
name="confirmPassword"
type="password"
value={passwordForm.confirmPassword}
onChange={handlePasswordChange}
required
/>
</div>
<Button type="submit" disabled={saving}>
{saving ? (
<>
<div className="mr-2 h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent"></div>
Enregistrement...
</>
) : (
<>
<Save className="mr-2 h-4 w-4" />
Changer le mot de passe
</>
)}
</Button>
</form>
</CardContent>
</Card>
</TabsContent>
</Tabs>
<div className="mt-8 space-y-6">
<div>
<h2 className="text-2xl font-bold">Mes recettes</h2>
<Separator className="my-4" />
{userRecipes.length === 0 ? (
<div className="rounded-lg border p-6 text-center">
<p className="text-muted-foreground">Vous n'avez pas encore créé de recettes</p>
<Button
className="mt-4"
onClick={() => navigate("/recipes/create")}
>
Créer une recette
</Button>
</div>
) : (
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{userRecipes.slice(0, 3).map(recipe => (
<Card key={recipe.id} className="overflow-hidden">
<div className="aspect-video w-full overflow-hidden">
<img
src={recipe.imageUrl || "/images/recipe-placeholder.jpg"}
alt={recipe.title}
className="h-full w-full object-cover"
/>
</div>
<CardHeader className="p-4">
<CardTitle className="text-lg">{recipe.title}</CardTitle>
</CardHeader>
<CardFooter className="p-4 pt-0">
<Button
variant="outline"
className="w-full"
onClick={() => navigate(`/recipes/${recipe.id}`)}
>
Voir la recette
</Button>
</CardFooter>
</Card>
))}
{userRecipes.length > 3 && (
<Button
variant="outline"
className="mt-4"
onClick={() => navigate("/my-recipes")}
>
Voir toutes mes recettes ({userRecipes.length})
</Button>
)}
</div>
)}
</div>
<div>
<h2 className="text-2xl font-bold">Mes favoris</h2>
<Separator className="my-4" />
{favoriteRecipes.length === 0 ? (
<div className="rounded-lg border p-6 text-center">
<p className="text-muted-foreground">Vous n'avez pas encore de recettes favorites</p>
<Button
className="mt-4"
onClick={() => navigate("/recipes")}
>
Explorer les recettes
</Button>
</div>
) : (
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{favoriteRecipes.slice(0, 3).map(recipe => (
<Card key={recipe.id} className="overflow-hidden">
<div className="aspect-video w-full overflow-hidden">
<img
src={recipe.imageUrl || "/images/recipe-placeholder.jpg"}
alt={recipe.title}
className="h-full w-full object-cover"
/>
</div>
<CardHeader className="p-4">
<CardTitle className="text-lg">{recipe.title}</CardTitle>
</CardHeader>
<CardFooter className="p-4 pt-0">
<Button
variant="outline"
className="w-full"
onClick={() => navigate(`/recipes/${recipe.id}`)}
>
Voir la recette
</Button>
</CardFooter>
</Card>
))}
{favoriteRecipes.length > 3 && (
<Button
variant="outline"
className="mt-4"
onClick={() => navigate("/favorites")}
>
Voir tous mes favoris ({favoriteRecipes.length})
</Button>
)}
</div>
)}
</div>
</div>
</div>
</div>
</div>
);
}