update frontend routes
This commit is contained in:
parent
45a26e269d
commit
bd5891ff1b
@ -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>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
52
frontend/src/components/RouteGuards.tsx
Normal file
52
frontend/src/components/RouteGuards.tsx
Normal 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;
|
||||
};
|
||||
@ -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,6 +46,8 @@ export function Header() {
|
||||
</div>
|
||||
|
||||
{/* Navigation desktop */}
|
||||
{
|
||||
isAuthenticated && (
|
||||
<nav className="hidden md:flex items-center gap-10">
|
||||
{filteredNavItems.map((item) => (
|
||||
<Link
|
||||
@ -69,6 +63,8 @@ export function Header() {
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
{/* Boutons d'authentification */}
|
||||
<div className="hidden md:flex items-center gap-4">
|
||||
@ -98,13 +94,16 @@ export function Header() {
|
||||
</div>
|
||||
|
||||
{/* Bouton menu mobile */}
|
||||
{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 */}
|
||||
{isAuthenticated && (
|
||||
<AnimatePresence>
|
||||
{isMobileMenuOpen && (
|
||||
<motion.div
|
||||
@ -172,6 +171,7 @@ export function Header() {
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
)}
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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")
|
||||
|
||||
41
frontend/src/hooks/useAuth.ts
Normal file
41
frontend/src/hooks/useAuth.ts
Normal 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;
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user