update frontend routes

This commit is contained in:
Arthur Barre 2025-03-13 22:04:04 +01:00
parent 45a26e269d
commit bd5891ff1b
8 changed files with 272 additions and 180 deletions

View File

@ -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 <div>Chargement de l'application...</div>;
}
return (
<BrowserRouter>
<MainLayout>
<Routes>
{/* Pages d'authentification */}
<Route path="/auth/register" element={<Register />} />
<Route path="/auth/login" element={<Login />} />
{/* Routes d'authentification */}
<Route path="/auth/register" element={<AuthRoute><Register /></AuthRoute>} />
<Route path="/auth/login" element={<AuthRoute><Login /></AuthRoute>} />
{/* Pages de recettes */}
<Route path="/recipes" element={<RecipeList />} />
<Route path="/recipes/:id" element={<RecipeDetail />} />
<Route path="/recipes/new" element={<RecipeForm />} />
{/* <Route path="/favorites" element={<Favorites />} /> */}
{/* Routes publiques */}
<Route path="/recipes" element={<ProtectedRoute><RecipeList /></ProtectedRoute>} />
<Route path="/recipes/:id" element={<ProtectedRoute><RecipeDetail /></ProtectedRoute>} />
{/* Autres pages */}
<Route path="/profile" element={<Profile />} />
<Route path="/" element={<Home />} />
{/* Routes protégées */}
<Route path="/recipes/new" element={<ProtectedRoute><RecipeForm /></ProtectedRoute>} />
<Route path="/profile" element={<ProtectedRoute><Profile /></ProtectedRoute>} />
{/* Route racine avec redirection conditionnelle */}
<Route path="/" element={isAuthenticated ? <Navigate to="/recipes" replace /> : <Home />} />
{/* Route de fallback pour les URLs non trouvées */}
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</MainLayout>
</BrowserRouter>

View File

@ -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;
}

View File

@ -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 <div>Chargement...</div>;
}
// Rediriger vers login si non authentifié
if (!isAuthenticated) {
return <Navigate to="/auth/login" replace />;
}
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 <div>Chargement...</div>;
}
// Rediriger vers recipes si déjà authentifié
if (isAuthenticated) {
return <Navigate to="/recipes" replace />;
}
return children;
};

View File

@ -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() {
</div>
{/* Navigation desktop */}
<nav className="hidden md:flex items-center gap-10">
{filteredNavItems.map((item) => (
<Link
key={item.path}
to={item.path}
className={cn(
"text-sm font-medium transition-colors hover:text-orange-500 flex items-center gap-1.5",
location.pathname === item.path ? "text-orange-500" : "text-muted-foreground",
)}
>
<item.icon className="h-4 w-4" />
{item.name}
</Link>
))}
</nav>
{
isAuthenticated && (
<nav className="hidden md:flex items-center gap-10">
{filteredNavItems.map((item) => (
<Link
key={item.path}
to={item.path}
className={cn(
"text-sm font-medium transition-colors hover:text-orange-500 flex items-center gap-1.5",
location.pathname === item.path ? "text-orange-500" : "text-muted-foreground",
)}
>
<item.icon className="h-4 w-4" />
{item.name}
</Link>
))}
</nav>
)
}
{/* Boutons d'authentification */}
<div className="hidden md:flex items-center gap-4">
@ -98,80 +94,84 @@ export function Header() {
</div>
{/* Bouton menu mobile */}
<Button variant="ghost" size="icon" className="md:hidden rounded-full" onClick={toggleMobileMenu}>
{isMobileMenuOpen ? <X className="h-6 w-6" /> : <Menu className="h-6 w-6" />}
</Button>
{isAuthenticated && (
<Button variant="ghost" size="icon" className="md:hidden rounded-full" onClick={toggleMobileMenu}>
{isMobileMenuOpen ? <X className="h-6 w-6" /> : <Menu className="h-6 w-6" />}
</Button>
)}
</div>
</div>
{/* Menu mobile */}
<AnimatePresence>
{isMobileMenuOpen && (
<motion.div
className="md:hidden border-b"
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: "auto" }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.2 }}
>
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-4 pb-6">
<nav className="flex flex-col space-y-4">
{filteredNavItems.map((item, index) => (
{isAuthenticated && (
<AnimatePresence>
{isMobileMenuOpen && (
<motion.div
className="md:hidden border-b"
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: "auto" }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.2 }}
>
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-4 pb-6">
<nav className="flex flex-col space-y-4">
{filteredNavItems.map((item, index) => (
<motion.div
key={item.path}
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: index * 0.05 }}
>
<Link
to={item.path}
className={cn(
"flex items-center gap-3 py-2 text-sm font-medium transition-colors hover:text-orange-500",
location.pathname === item.path ? "text-orange-500" : "text-muted-foreground",
)}
onClick={() => setIsMobileMenuOpen(false)}
>
<item.icon className="h-5 w-5" />
{item.name}
</Link>
</motion.div>
))}
<motion.div
key={item.path}
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: index * 0.05 }}
transition={{ delay: filteredNavItems.length * 0.05 }}
className="pt-2"
>
<Link
to={item.path}
className={cn(
"flex items-center gap-3 py-2 text-sm font-medium transition-colors hover:text-orange-500",
location.pathname === item.path ? "text-orange-500" : "text-muted-foreground",
)}
onClick={() => setIsMobileMenuOpen(false)}
>
<item.icon className="h-5 w-5" />
{item.name}
</Link>
{isAuthenticated ? (
<Button
variant="outline"
onClick={handleLogout}
className="w-full border-orange-200 hover:bg-orange-50 hover:text-orange-600 dark:border-orange-800 dark:hover:bg-orange-900/20 dark:hover:text-orange-400"
>
<LogOut className="mr-2 h-4 w-4" />
Déconnexion
</Button>
) : (
<div className="flex flex-col space-y-2">
<Link to="/auth/login" onClick={() => setIsMobileMenuOpen(false)}>
<Button variant="ghost" className="w-full hover:text-orange-500">
Connexion
</Button>
</Link>
<Link to="/auth/register" onClick={() => setIsMobileMenuOpen(false)}>
<Button className="w-full bg-gradient-to-r from-orange-500 to-amber-500 hover:from-orange-600 hover:to-amber-600 text-white">
S'inscrire
</Button>
</Link>
</div>
)}
</motion.div>
))}
<motion.div
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: filteredNavItems.length * 0.05 }}
className="pt-2"
>
{isAuthenticated ? (
<Button
variant="outline"
onClick={handleLogout}
className="w-full border-orange-200 hover:bg-orange-50 hover:text-orange-600 dark:border-orange-800 dark:hover:bg-orange-900/20 dark:hover:text-orange-400"
>
<LogOut className="mr-2 h-4 w-4" />
Déconnexion
</Button>
) : (
<div className="flex flex-col space-y-2">
<Link to="/auth/login" onClick={() => setIsMobileMenuOpen(false)}>
<Button variant="ghost" className="w-full hover:text-orange-500">
Connexion
</Button>
</Link>
<Link to="/auth/register" onClick={() => setIsMobileMenuOpen(false)}>
<Button className="w-full bg-gradient-to-r from-orange-500 to-amber-500 hover:from-orange-600 hover:to-amber-600 text-white">
S'inscrire
</Button>
</Link>
</div>
)}
</motion.div>
</nav>
</div>
</motion.div>
)}
</AnimatePresence>
</nav>
</div>
</motion.div>
)}
</AnimatePresence>
)}
</header>
)
}

View File

@ -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")

View File

@ -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<boolean>(false);
const [isLoading, setIsLoading] = useState<boolean>(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;

View File

@ -13,7 +13,6 @@ import PastaVongole from "@/assets/pasta-alla-vongole-home.jpeg"
export default function Home() {
return (
<div className="md:space-y-24 space-y-4 pb-16">
222
<section className="py-12 md:py-20 flex justify-center">
<div className="container px-4 md:px-6">
<div className="grid gap-6 lg:grid-cols-2 lg:gap-12 items-center">
@ -37,20 +36,20 @@ export default function Home() {
secondes. Fini le gaspillage alimentaire !
</p>
<div className="flex flex-col gap-3 min-[400px]:flex-row">
<Link to="/recipes">
<Link to="/auth/register">
<Button className="bg-gradient-to-r from-orange-500 to-amber-500 hover:from-orange-600 hover:to-amber-600 text-white">
Commencer maintenant
<ArrowRight className="ml-2 h-4 w-4" />
</Button>
</Link>
<Link to="/auth/register">
{/* <Link to="/auth/register">
<Button
variant="outline"
className="border-orange-200 hover:bg-orange-50 hover:text-orange-600 dark:border-orange-800 dark:hover:bg-orange-900/20 dark:hover:text-orange-400"
>
Créer un compte
</Button>
</Link>
</Link> */}
</div>
</motion.div>
<motion.div
@ -429,11 +428,11 @@ export default function Home() {
<ArrowRight className="ml-2 h-4 w-4" />
</Button>
</Link>
<Link to="/recipes">
{/* <Link to="/recipes">
<Button variant="outline" className="border-white text-white hover:bg-white/20">
Explorer les recettes
</Button>
</Link>
</Link> */}
</div>
</div>
</motion.div>

View File

@ -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<Recipe | null>(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<Set<number>>(new Set())
const [checkedSteps, setCheckedSteps] = useState<Set<number>>(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 <RecipeDetailLoader />
}
@ -222,7 +217,7 @@ export default function RecipeDetail() {
<Button variant="ghost" size="icon" className="rounded-full" onClick={handleShare}>
<Share2 className="h-4 w-4" />
</Button>
{/*
<Button
variant={isFavorite ? "default" : "ghost"}
size="icon"
@ -231,7 +226,7 @@ export default function RecipeDetail() {
disabled={addingToFavorites}
>
<Heart className="h-4 w-4" />
</Button>
</Button> */}
</div>
</div>
</div>
@ -371,41 +366,33 @@ export default function RecipeDetail() {
{recipe.instructions && recipe.instructions.length > 0 ? (
recipe.instructions.map((instruction, index) => (
<li key={index} className="flex">
<Checkbox
{/* <Checkbox
id={`step-${index}`}
checked={checkedSteps.has(index)}
onCheckedChange={() => toggleStep(index)}
className="mr-3 mt-1 data-[state=checked]:bg-orange-500 data-[state=checked]:border-orange-500"
/>
/> */}
<div className="flex-1">
<label
htmlFor={`step-${index}`}
className={`flex items-center font-medium mb-2 cursor-pointer ${checkedSteps.has(index) ? "line-through text-muted-foreground" : ""}`}
>
Étape {index + 1}
{checkedSteps.has(index) && <CheckCircle2 className="ml-2 h-4 w-4 text-green-500" />}
</label>
<p className={checkedSteps.has(index) ? "text-muted-foreground" : ""}>{instruction}</p>
<p className={checkedSteps.has(index) ? "text-muted-foreground" : ""}>
{instruction}
{checkedSteps.has(index) && <CheckCircle2 className="ml-2 h-4 w-4 text-green-500 inline" />}
</p>
</div>
</li>
))
) : (
<li className="flex">
<Checkbox
{/* <Checkbox
id="step-single"
checked={checkedSteps.has(0)}
onCheckedChange={() => toggleStep(0)}
className="mr-3 mt-1 data-[state=checked]:bg-orange-500 data-[state=checked]:border-orange-500"
/>
/> */}
<div className="flex-1">
<label
htmlFor="step-single"
className={`flex items-center font-medium mb-2 cursor-pointer ${checkedSteps.has(0) ? "line-through text-muted-foreground" : ""}`}
>
Instructions
{checkedSteps.has(0) && <CheckCircle2 className="ml-2 h-4 w-4 text-green-500" />}
</label>
<p className={checkedSteps.has(0) ? "text-muted-foreground" : ""}>{recipe.generatedRecipe}</p>
<p className={checkedSteps.has(0) ? "text-muted-foreground" : ""}>
{recipe.generatedRecipe}
{checkedSteps.has(0) && <CheckCircle2 className="ml-2 h-4 w-4 text-green-500 inline" />}
</p>
</div>
</li>
)}