diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index f1785b7..667a52c 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,4 +1,4 @@ -import { BrowserRouter, Routes, Route } from 'react-router-dom' +import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom' import './App.css' import Register from './pages/Auth/Register' import Login from './pages/Auth/Login' @@ -9,25 +9,37 @@ import Profile from './pages/Profile' import Home from './pages/Home' import { MainLayout } from './layouts/MainLayout' import RecipeForm from "@/pages/Recipes/RecipeForm" +import useAuth from '@/hooks/useAuth' +import { ProtectedRoute, AuthRoute } from '@/components/RouteGuards' function App() { + const { isAuthenticated, isLoading } = useAuth(); + + if (isLoading) { + return
Chargement de l'application...
; + } + return ( - {/* Pages d'authentification */} - } /> - } /> + {/* Routes d'authentification */} + } /> + } /> - {/* Pages de recettes */} - } /> - } /> - } /> - {/* } /> */} + {/* Routes publiques */} + } /> + } /> - {/* Autres pages */} - } /> - } /> + {/* Routes protégées */} + } /> + } /> + + {/* Route racine avec redirection conditionnelle */} + : } /> + + {/* Route de fallback pour les URLs non trouvées */} + } /> diff --git a/frontend/src/api/recipe.ts b/frontend/src/api/recipe.ts index 7382d91..b08dd43 100644 --- a/frontend/src/api/recipe.ts +++ b/frontend/src/api/recipe.ts @@ -3,10 +3,11 @@ import axios from 'axios'; export interface Recipe { id: string; - title: string; - ingredients: string; - generatedRecipe: string; - createdAt: string; + title?: string; + instructions: string[]; + ingredients?: string; + generatedRecipe?: string; + createdAt?: string; audioUrl?: string; } diff --git a/frontend/src/components/RouteGuards.tsx b/frontend/src/components/RouteGuards.tsx new file mode 100644 index 0000000..7f70c4f --- /dev/null +++ b/frontend/src/components/RouteGuards.tsx @@ -0,0 +1,52 @@ +import { Navigate } from 'react-router-dom'; +import useAuth from '@/hooks/useAuth'; + +interface RouteGuardProps { + children: JSX.Element; +} + +/** + * Protège les routes qui nécessitent une authentification + * + * Règles: + * 1. Si l'utilisateur n'est pas authentifié, redirection vers /auth/login + * 2. Si l'utilisateur est authentifié, affiche le composant enfant + */ +export const ProtectedRoute = ({ children }: RouteGuardProps): JSX.Element => { + const { isAuthenticated, isLoading } = useAuth(); + + // Afficher un loader pendant la vérification + if (isLoading) { + return
Chargement...
; + } + + // Rediriger vers login si non authentifié + if (!isAuthenticated) { + return ; + } + + return children; +}; + +/** + * Protège les routes d'authentification (login, register) + * + * Règles: + * 1. Si l'utilisateur est déjà authentifié, redirection vers /recipes + * 2. Si l'utilisateur n'est pas authentifié, affiche le composant enfant + */ +export const AuthRoute = ({ children }: RouteGuardProps): JSX.Element => { + const { isAuthenticated, isLoading } = useAuth(); + + // Afficher un loader pendant la vérification + if (isLoading) { + return
Chargement...
; + } + + // Rediriger vers recipes si déjà authentifié + if (isAuthenticated) { + return ; + } + + return children; +}; \ No newline at end of file diff --git a/frontend/src/components/header.tsx b/frontend/src/components/header.tsx index addef56..c4428b0 100644 --- a/frontend/src/components/header.tsx +++ b/frontend/src/components/header.tsx @@ -7,20 +7,13 @@ import { Menu, X, LogOut, User, Heart, Home, BookOpen } from "lucide-react" import { cn } from "@/lib/utils" import { motion, AnimatePresence } from "framer-motion" import { Logo } from "@/components/illustrations/Logo" +import useAuth from "@/hooks/useAuth" export function Header() { - const [isAuthenticated, setIsAuthenticated] = useState(false) + const { isAuthenticated } = useAuth() const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false) const location = useLocation() - useEffect(() => { - // Vérifier si l'utilisateur est authentifié - const token = localStorage.getItem("token") - setIsAuthenticated(!!token) - - // Fermer le menu mobile lors d'un changement de route - setIsMobileMenuOpen(false) - }, [location]) const toggleMobileMenu = () => { setIsMobileMenuOpen(!isMobileMenuOpen) @@ -28,7 +21,6 @@ export function Header() { const handleLogout = () => { localStorage.removeItem("token") - setIsAuthenticated(false) setIsMobileMenuOpen(false) window.location.href = "/auth/login" } @@ -36,9 +28,9 @@ export function Header() { const navItems = [ { name: "Accueil", path: "/", icon: Home, public: true }, { name: "Recettes", path: "/recipes", icon: BookOpen, public: true }, - // { name: "Mes recettes", path: "/my-recipes", icon: BookOpen, public: false }, - // { name: "Favoris", path: "/favorites", icon: Heart, public: false }, - // { name: "Profil", path: "/profile", icon: User, public: false }, + { name: "Mes recettes", path: "/recipes", icon: BookOpen, public: false }, + { name: "Favoris", path: "/favorites", icon: Heart, public: false }, + { name: "Profil", path: "/profile", icon: User, public: false }, ] const filteredNavItems = navItems.filter((item) => item.public || isAuthenticated) @@ -54,21 +46,25 @@ export function Header() { {/* Navigation desktop */} - + { + isAuthenticated && ( + + ) + } {/* Boutons d'authentification */}
@@ -98,80 +94,84 @@ export function Header() {
{/* Bouton menu mobile */} - + {isAuthenticated && ( + + )} {/* Menu mobile */} - - {isMobileMenuOpen && ( - -
- +
+
+ )} +
+ )} ) } diff --git a/frontend/src/components/login-form.tsx b/frontend/src/components/login-form.tsx index 350868f..2176b96 100644 --- a/frontend/src/components/login-form.tsx +++ b/frontend/src/components/login-form.tsx @@ -29,7 +29,7 @@ export function LoginForm({ throw new Error("Échec de la connexion") } - navigate("/") + navigate("/recipes") } catch (err) { console.error("Erreur de connexion:", err); setError(err instanceof Error ? err.message : "Une erreur est survenue") @@ -52,7 +52,7 @@ export function LoginForm({ localStorage.setItem("token", response.token) - navigate("/") + navigate("/recipes") } catch (err) { console.error("Erreur de connexion Google:", err) setError(err instanceof Error ? err.message : "Une erreur est survenue") diff --git a/frontend/src/hooks/useAuth.ts b/frontend/src/hooks/useAuth.ts new file mode 100644 index 0000000..9cbe7fb --- /dev/null +++ b/frontend/src/hooks/useAuth.ts @@ -0,0 +1,41 @@ +import { useState, useEffect } from 'react'; + +interface AuthHook { + isAuthenticated: boolean; + isLoading: boolean; + checkAuth: () => boolean; +} + +/** + * Hook pour gérer l'authentification + * + * Règles: + * 1. Un utilisateur est considéré comme authentifié s'il a un token valide dans localStorage + * 2. Le hook vérifie l'authentification au chargement et fournit un état de chargement + * 3. Expose une méthode pour vérifier l'authentification à la demande + */ +const useAuth = (): AuthHook => { + const [isAuthenticated, setIsAuthenticated] = useState(false); + const [isLoading, setIsLoading] = useState(true); + + // Fonction pour vérifier l'authentification + const checkAuth = (): boolean => { + const token = localStorage.getItem('token'); + // On pourrait ajouter une vérification de validité du token ici + return !!token; + }; + + useEffect(() => { + // Vérifier l'authentification au chargement + setIsAuthenticated(checkAuth()); + setIsLoading(false); + }, []); + + return { + isAuthenticated, + isLoading, + checkAuth + }; +}; + +export default useAuth; \ No newline at end of file diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index 8756ac5..ca77113 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -13,7 +13,6 @@ import PastaVongole from "@/assets/pasta-alla-vongole-home.jpeg" export default function Home() { return (
- 222
@@ -37,20 +36,20 @@ export default function Home() { secondes. Fini le gaspillage alimentaire !

- + - + {/* - + */}
- + {/* - + */}
diff --git a/frontend/src/pages/Recipes/RecipeDetail.tsx b/frontend/src/pages/Recipes/RecipeDetail.tsx index 54bd5d4..5c7122c 100644 --- a/frontend/src/pages/Recipes/RecipeDetail.tsx +++ b/frontend/src/pages/Recipes/RecipeDetail.tsx @@ -11,8 +11,6 @@ import { Heart, Share2, ArrowLeft, - Trash2, - Edit, Printer, CheckCircle2, Timer, @@ -32,8 +30,8 @@ export default function RecipeDetail() { const [recipe, setRecipe] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState("") - const [isFavorite, setIsFavorite] = useState(false) - const [addingToFavorites, setAddingToFavorites] = useState(false) + // const [isFavorite, setIsFavorite] = useState(false) + // const [addingToFavorites, setAddingToFavorites] = useState(false) const [checkedIngredients, setCheckedIngredients] = useState>(new Set()) const [checkedSteps, setCheckedSteps] = useState>(new Set()) const [showScrollTop, setShowScrollTop] = useState(false) @@ -68,8 +66,8 @@ export default function RecipeDetail() { // ✅ GET FAVORITE RECIPES & CHECK IF FAVORITE try { - const favorites = await recipeService.getFavoriteRecipes() - setIsFavorite(favorites.some((fav) => fav.id === id)) + // const favorites = await recipeService.getFavoriteRecipes() + // setIsFavorite(favorites.some((fav) => fav.id === id)) } catch (favError) { // Ignorer les erreurs de favoris pour ne pas bloquer l'affichage console.log("Impossible de vérifier les favoris:", favError) @@ -97,42 +95,42 @@ export default function RecipeDetail() { return () => window.removeEventListener("scroll", handleScroll) }, [id]) - const handleToggleFavorite = async () => { - if (!id || !recipe) return + // const handleToggleFavorite = async () => { + // if (!id || !recipe) return - try { - setAddingToFavorites(true) + // try { + // setAddingToFavorites(true) - if (isFavorite) { - // ✅ REMOVE FROM FAVORITES - await recipeService.removeFromFavorites(id) - setIsFavorite(false) - } else { - // ✅ ADD TO FAVORITES - await recipeService.addToFavorites(id) - setIsFavorite(true) - } - } catch (err) { - console.error("Erreur lors de la modification des favoris:", err) - // Ne pas afficher d'erreur à l'utilisateur pour cette fonctionnalité - } finally { - setAddingToFavorites(false) - } - } + // if (isFavorite) { + // // ✅ REMOVE FROM FAVORITES + // await recipeService.removeFromFavorites(id) + // setIsFavorite(false) + // } else { + // // ✅ ADD TO FAVORITES + // await recipeService.addToFavorites(id) + // setIsFavorite(true) + // } + // } catch (err) { + // console.error("Erreur lors de la modification des favoris:", err) + // // Ne pas afficher d'erreur à l'utilisateur pour cette fonctionnalité + // } finally { + // setAddingToFavorites(false) + // } + // } - const handleDeleteRecipe = async () => { - if (!id) return - if (!window.confirm("Êtes-vous sûr de vouloir supprimer cette recette ?")) return + // const handleDeleteRecipe = async () => { + // if (!id) return + // if (!window.confirm("Êtes-vous sûr de vouloir supprimer cette recette ?")) return - try { - // ✅ DELETE RECIPE - await recipeService.deleteRecipe(id) - navigate("/recipes") - } catch (err) { - console.error("Erreur lors de la suppression:", err) - // Optionnel: afficher un message d'erreur à l'utilisateur - } - } + // try { + // // ✅ DELETE RECIPE + // await recipeService.deleteRecipe(id) + // navigate("/recipes") + // } catch (err) { + // console.error("Erreur lors de la suppression:", err) + // // Optionnel: afficher un message d'erreur à l'utilisateur + // } + // } const handleShare = () => { if (!recipe) return @@ -177,9 +175,6 @@ export default function RecipeDetail() { window.scrollTo({ top: 0, behavior: "smooth" }) } - // ======================================== - // === LOADING & ERROR STATES ============ - // ======================================== if (loading) { return } @@ -222,7 +217,7 @@ export default function RecipeDetail() { - + {/* + */}
@@ -371,41 +366,33 @@ export default function RecipeDetail() { {recipe.instructions && recipe.instructions.length > 0 ? ( recipe.instructions.map((instruction, index) => (
  • - toggleStep(index)} className="mr-3 mt-1 data-[state=checked]:bg-orange-500 data-[state=checked]:border-orange-500" - /> + /> */}
    - -

    {instruction}

    +

    + {instruction} + {checkedSteps.has(index) && } +

  • )) ) : (
  • - toggleStep(0)} className="mr-3 mt-1 data-[state=checked]:bg-orange-500 data-[state=checked]:border-orange-500" - /> + /> */}
    - -

    {recipe.generatedRecipe}

    +

    + {recipe.generatedRecipe} + {checkedSteps.has(0) && } +

  • )}