diff --git a/frontend/src/pages/Profile.tsx b/frontend/src/pages/Profile.tsx index 9fdfbcd..26b3c54 100644 --- a/frontend/src/pages/Profile.tsx +++ b/frontend/src/pages/Profile.tsx @@ -1,20 +1,46 @@ import { useState, useEffect } from "react"; import { useNavigate } from "react-router-dom"; +import { motion, AnimatePresence } from "framer-motion"; 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, Mail, ChefHat } from "lucide-react"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Avatar, AvatarFallback } from "@/components/ui/avatar"; +import { Badge } from "@/components/ui/badge"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { + AlertCircle, + Save, + User as UserIcon, + Lock, + LogOut, + Trash2, + Mail, + ChefHat, + CheckCircle2, + X, + Calendar, + Sparkles, + BookOpen, + AlertTriangle, +} from "lucide-react"; import { Checkbox } from "@/components/ui/checkbox"; import { recipeService, Recipe } from "@/api/recipe"; import userService from "@/api/user"; -// Types pour les données utilisateur +// --------------------------------------------------------------------------- +// Types & constants +// --------------------------------------------------------------------------- + interface User { id: string; email: string; @@ -30,17 +56,24 @@ interface User { } const EQUIPMENT_OPTIONS = [ - { key: "plaque", label: "Plaque / gazinière" }, - { key: "four", label: "Four" }, - { key: "micro-ondes", label: "Micro-ondes" }, - { key: "mixeur", label: "Mixeur / blender" }, - { key: "robot", label: "Robot pâtissier" }, - { key: "friteuse", label: "Friteuse à air" }, - { key: "barbecue", label: "Barbecue / plancha" }, - { key: "cuiseur-vapeur", label: "Cuiseur vapeur" }, + { key: "plaque", label: "Plaque", emoji: "🔥" }, + { key: "four", label: "Four", emoji: "♨️" }, + { key: "micro-ondes", label: "Micro-ondes", emoji: "📡" }, + { key: "mixeur", label: "Mixeur", emoji: "🌀" }, + { key: "robot", label: "Robot pâtissier", emoji: "🍰" }, + { key: "friteuse", label: "Friteuse à air", emoji: "🍟" }, + { key: "barbecue", label: "Barbecue", emoji: "🔥" }, + { key: "cuiseur-vapeur", label: "Cuiseur vapeur", emoji: "💨" }, ]; -// Service utilisateur +const DIET_LABELS: Record = { + none: "Omnivore", + vegetarian: "Végétarien", + vegan: "Végan", + pescatarian: "Pescétarien", +}; + +// --------------------------------------------------------------------------- export default function Profile() { const navigate = useNavigate(); @@ -50,53 +83,33 @@ export default function Profile() { const [saving, setSaving] = useState(false); const [error, setError] = useState(""); const [success, setSuccess] = useState(""); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [deletePassword, setDeletePassword] = useState(""); - // États pour le formulaire de profil - const [profileForm, setProfileForm] = useState({ - name: "" - }); - - // États pour le formulaire de mot de passe + const [profileForm, setProfileForm] = useState({ name: "" }); const [passwordForm, setPasswordForm] = useState({ currentPassword: "", newPassword: "", - confirmPassword: "" + confirmPassword: "", }); - - // États pour le formulaire de changement d'email - const [emailForm, setEmailForm] = useState({ - newEmail: "", - password: "" - }); - - // Préférences culinaires - const [prefsForm, setPrefsForm] = useState<{ - dietaryPreference: string; - allergies: string; - maxCookingTime: string; - equipment: string[]; - cuisinePreference: string; - servingsDefault: string; - }>({ + const [emailForm, setEmailForm] = useState({ newEmail: "", password: "" }); + const [prefsForm, setPrefsForm] = useState({ dietaryPreference: "none", allergies: "", maxCookingTime: "", - equipment: [], + equipment: [] as string[], cuisinePreference: "", servingsDefault: "", }); + // --- Load user data --- useEffect(() => { const fetchUserData = async () => { try { setLoading(true); - - // Récupérer les données de l'utilisateur const userData = await userService.getCurrentUser(); setUser(userData); - setProfileForm({ - name: userData.name || "" - }); + setProfileForm({ name: userData.name || "" }); setPrefsForm({ dietaryPreference: userData.dietaryPreference || "none", allergies: userData.allergies || "", @@ -105,15 +118,11 @@ export default function Profile() { cuisinePreference: userData.cuisinePreference || "", servingsDefault: userData.servingsDefault ? String(userData.servingsDefault) : "", }); - - // Récupérer les recettes de l'utilisateur const recipes = await recipeService.getRecipes(); setUserRecipes(recipes); } 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"); @@ -122,37 +131,28 @@ export default function Profile() { setLoading(false); } }; - fetchUserData(); }, [navigate]); - const handleProfileChange = (e: React.ChangeEvent) => { - const { name, value } = e.target; - setProfileForm(prev => ({ ...prev, [name]: value })); - }; + // Auto-dismiss success messages + useEffect(() => { + if (!success) return; + const id = setTimeout(() => setSuccess(""), 4000); + return () => clearTimeout(id); + }, [success]); - const handlePasswordChange = (e: React.ChangeEvent) => { - const { name, value } = e.target; - setPasswordForm(prev => ({ ...prev, [name]: value })); - }; - - const handleEmailChange = (e: React.ChangeEvent) => { - const { name, value } = e.target; - setEmailForm(prev => ({ ...prev, [name]: value })); - }; + // --- Handlers --- 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); + setSuccess("Profil mis à jour"); + } catch { setError("Impossible de mettre à jour le profil"); } finally { setSaving(false); @@ -163,7 +163,6 @@ export default function Profile() { e.preventDefault(); setError(""); setSuccess(""); - try { setSaving(true); await userService.updatePreferences({ @@ -175,8 +174,7 @@ export default function Profile() { servingsDefault: prefsForm.servingsDefault ? parseInt(prefsForm.servingsDefault, 10) : null, }); setSuccess("Préférences culinaires mises à jour"); - } catch (err) { - console.error("Erreur lors de la mise à jour des préférences:", err); + } catch { setError("Impossible de mettre à jour les préférences"); } finally { setSaving(false); @@ -196,34 +194,24 @@ export default function Profile() { 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; } - - // Vérifier la longueur du mot de passe if (passwordForm.newPassword.length < 8) { setError("Le mot de passe doit contenir au moins 8 caractères"); return; } - try { setSaving(true); await userService.changePassword({ currentPassword: passwordForm.currentPassword, - newPassword: passwordForm.newPassword + 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. Vérifiez que votre mot de passe actuel est correct."); + setSuccess("Mot de passe modifié"); + setPasswordForm({ currentPassword: "", newPassword: "", confirmPassword: "" }); + } catch { + setError("Mot de passe actuel incorrect"); } finally { setSaving(false); } @@ -233,37 +221,22 @@ export default function Profile() { e.preventDefault(); setError(""); setSuccess(""); - - // Vérifier que l'email est valide const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(emailForm.newEmail)) { - setError("Veuillez entrer une adresse email valide"); + setError("Adresse email invalide"); return; } - try { setSaving(true); await userService.changeEmail({ newEmail: emailForm.newEmail, - password: emailForm.password + password: emailForm.password, }); - setSuccess("Email modifié avec succès"); - - // Mettre à jour l'utilisateur avec le nouvel email - if (user) { - setUser({ - ...user, - email: emailForm.newEmail - }); - } - - setEmailForm({ - newEmail: "", - password: "" - }); - } catch (err) { - console.error("Erreur lors du changement d'email:", err); - setError("Impossible de changer l'email. Vérifiez que votre mot de passe est correct."); + setSuccess("Email modifié"); + if (user) setUser({ ...user, email: emailForm.newEmail }); + setEmailForm({ newEmail: "", password: "" }); + } catch { + setError("Mot de passe incorrect"); } finally { setSaving(false); } @@ -272,60 +245,57 @@ export default function Profile() { 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"); - } + } catch {/* ignore */} + localStorage.removeItem("token"); + navigate("/auth/login"); }; const handleDeleteAccount = async () => { - const password = prompt("Pour confirmer la suppression de votre compte, veuillez entrer votre mot de passe:"); - - if (!password) return; - - if (!window.confirm("Êtes-vous sûr de vouloir supprimer votre compte ? Cette action est irréversible.")) { - return; - } - try { - await userService.deleteAccount(password); + await userService.deleteAccount(deletePassword); localStorage.removeItem("token"); navigate("/auth/login"); - } catch (err) { - console.error("Erreur lors de la suppression du compte:", err); - setError("Impossible de supprimer le compte. Vérifiez que votre mot de passe est correct."); + } catch { + setError("Mot de passe incorrect"); + setDeleteDialogOpen(false); } }; + // --- Render: loading --- if (loading) { return ( -
- {/* Hero profil skeleton */} +
-
+
-
+
- {/* Tabs skeleton */} -
- {/* Card skeleton */} -
+
+
+
+
+
+
+
); } + // --- Render: error --- if (!user) { return ( -
-

Erreur

-

Utilisateur non trouvé ou non connecté

+
+
+ +
+

Non connecté

+

+ Vous devez être connecté pour accéder à cette page. +

-
- - {error && ( - - - Erreur - {error} - - )} - - {success && ( - - Succès - {success} - - )} - -
-
- - - Informations - Vos informations de compte - - - - - - {user.name ? user.name[0].toUpperCase() : user.email[0].toUpperCase()} - - -
-

{user.name || "Utilisateur"}

-

{user.email}

+
+ {/* --- Hero --- */} + +
+ {/* Avatar */} +
+ + + {initial} + + + {isPremium && ( +
+
-
-

Membre depuis

-

{new Date(user.createdAt).toLocaleDateString()}

+ )} +
+ + {/* Infos */} +
+
+
+

+ {user.name || "Utilisateur"} +

+

+ {user.email} +

- {user.subscription && ( -
-

Abonnement

-

{user.subscription}

-
- )} - - - - -
+
-
- - - - - Profil - - - - Cuisine - - - - Email - - - - Sécurité - - - - - - - Informations du profil - - Mettez à jour vos informations personnelles - - - -
-
- - -
- -
-
-
-
- - - - - Préférences culinaires - - Ces informations sont transmises au chef IA pour personnaliser chaque recette - - - -
- {/* Régime */} -
- - -
- - {/* Allergies */} -
- - - setPrefsForm({ ...prefsForm, allergies: e.target.value }) - } - /> -

- Séparées par des virgules. Le chef les évitera impérativement. -

-
- - {/* Temps max */} -
-
- - - setPrefsForm({ ...prefsForm, maxCookingTime: e.target.value }) - } - /> -
-
- - - setPrefsForm({ ...prefsForm, servingsDefault: e.target.value }) - } - /> -
-
- - {/* Cuisine préférée */} -
- - - setPrefsForm({ ...prefsForm, cuisinePreference: e.target.value }) - } - /> -
- - {/* Équipement */} -
- -
- {EQUIPMENT_OPTIONS.map((opt) => ( -
- toggleEquipment(opt.key)} - /> - -
- ))} -
-

- Le chef n'utilisera que des techniques compatibles avec ton équipement. -

-
- - -
-
-
-
- - - - - Changer d'email - - Mettez à jour votre adresse email - - - -
-
- - -
-
- - -
- -
-
-
-
- - - - - Sécurité du compte - - Mettez à jour votre mot de passe - - - -
-
- - -
-
- - -
-
- - -
- -
-
-
-
-
- -
-
-

Mes recettes

- - - {userRecipes.length === 0 ? ( -
-

Vous n'avez pas encore créé de recettes

- -
- ) : ( -
- {userRecipes.slice(0, 3).map(recipe => ( - -
- {recipe.title} -
- - {recipe.title} - - - - -
- ))} - - {userRecipes.length > 3 && ( - - )} -
- )} + {/* Badges */} +
+ + {isPremium ? ( + <> + + {user.subscription} + + ) : ( + "Plan gratuit" + )} + + + + Depuis {memberSince} + + + + {userRecipes.length} recette{userRecipes.length > 1 ? "s" : ""} +
-
+ + + {/* --- Alerts --- */} + + {error && ( + +
+ +
{error}
+ +
+
+ )} + {success && ( + +
+ +
{success}
+ +
+
+ )} +
+ + {/* --- Tabs --- */} + + + + + Cuisine + + + + Profil + + + + Email + + + + Sécurité + + + + {/* --- Tab Cuisine --- */} + + + +
+
+ + + Préférences culinaires + + + Ces infos sont envoyées au chef Antoine pour chaque nouvelle recette + +
+ {hasPrefs && ( + + + Actif + + )} +
+
+ +
+ {/* Régime */} +
+ +
+ {Object.entries(DIET_LABELS).map(([key, label]) => ( + + ))} +
+
+ + {/* Allergies */} +
+ + setPrefsForm({ ...prefsForm, allergies: e.target.value })} + className="rounded-xl h-11" + /> +

+ Séparées par des virgules. Le chef les évitera impérativement. +

+
+ + {/* Temps + portions */} +
+
+ +
+ setPrefsForm({ ...prefsForm, maxCookingTime: e.target.value })} + className="rounded-xl h-11 pr-12" + /> + + min + +
+
+
+ +
+ setPrefsForm({ ...prefsForm, servingsDefault: e.target.value })} + className="rounded-xl h-11 pr-16" + /> + + pers. + +
+
+
+ + {/* Cuisine */} +
+ + setPrefsForm({ ...prefsForm, cuisinePreference: e.target.value })} + className="rounded-xl h-11" + /> +
+ + {/* Équipement */} +
+ +
+ {EQUIPMENT_OPTIONS.map((opt) => { + const active = prefsForm.equipment.includes(opt.key); + return ( + + ); + })} +
+

+ Le chef n'utilisera que des techniques compatibles. +

+
+ +
+ +
+
+
+
+
+ + {/* --- Tab Profile --- */} + + + + Informations du profil + + Mettez à jour vos informations personnelles + + + +
+
+ + setProfileForm({ name: e.target.value })} + required + className="rounded-xl h-11" + /> +
+ +
+
+
+
+ + {/* --- Tab Email --- */} + + + + Changer d'adresse email + + Votre email actuel : {user.email} + + + +
+
+ + setEmailForm({ ...emailForm, newEmail: e.target.value })} + required + className="rounded-xl h-11" + /> +
+
+ + setEmailForm({ ...emailForm, password: e.target.value })} + required + className="rounded-xl h-11" + /> +
+ +
+
+
+
+ + {/* --- Tab Security --- */} + + + + Sécurité du compte + + Choisissez un mot de passe d'au moins 8 caractères + + + +
+
+ + setPasswordForm({ ...passwordForm, currentPassword: e.target.value })} + required + className="rounded-xl h-11" + /> +
+
+ + setPasswordForm({ ...passwordForm, newPassword: e.target.value })} + required + className="rounded-xl h-11" + /> +
+
+ + setPasswordForm({ ...passwordForm, confirmPassword: e.target.value })} + required + className="rounded-xl h-11" + /> +
+ +
+
+
+
+
+ + {/* --- Danger zone --- */} + +
+
+
+
+ +
+
+

+ Zone de danger +

+

+ La suppression de votre compte est définitive et toutes vos recettes seront effacées. +

+
+
+ +
+
+
+ + {/* --- Delete account dialog --- */} + + + + + + Supprimer votre compte ? + + + Cette action est définitive. Toutes vos recettes, préférences et + données personnelles seront supprimées de nos serveurs. + + +
+ + setDeletePassword(e.target.value)} + className="rounded-xl" + /> +
+ + + + +
+
); -} \ No newline at end of file +}