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 './App.css'
|
||||||
import Register from './pages/Auth/Register'
|
import Register from './pages/Auth/Register'
|
||||||
import Login from './pages/Auth/Login'
|
import Login from './pages/Auth/Login'
|
||||||
@ -9,25 +9,37 @@ import Profile from './pages/Profile'
|
|||||||
import Home from './pages/Home'
|
import Home from './pages/Home'
|
||||||
import { MainLayout } from './layouts/MainLayout'
|
import { MainLayout } from './layouts/MainLayout'
|
||||||
import RecipeForm from "@/pages/Recipes/RecipeForm"
|
import RecipeForm from "@/pages/Recipes/RecipeForm"
|
||||||
|
import useAuth from '@/hooks/useAuth'
|
||||||
|
import { ProtectedRoute, AuthRoute } from '@/components/RouteGuards'
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
const { isAuthenticated, isLoading } = useAuth();
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <div>Chargement de l'application...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<MainLayout>
|
<MainLayout>
|
||||||
<Routes>
|
<Routes>
|
||||||
{/* Pages d'authentification */}
|
{/* Routes d'authentification */}
|
||||||
<Route path="/auth/register" element={<Register />} />
|
<Route path="/auth/register" element={<AuthRoute><Register /></AuthRoute>} />
|
||||||
<Route path="/auth/login" element={<Login />} />
|
<Route path="/auth/login" element={<AuthRoute><Login /></AuthRoute>} />
|
||||||
|
|
||||||
{/* Pages de recettes */}
|
{/* Routes publiques */}
|
||||||
<Route path="/recipes" element={<RecipeList />} />
|
<Route path="/recipes" element={<ProtectedRoute><RecipeList /></ProtectedRoute>} />
|
||||||
<Route path="/recipes/:id" element={<RecipeDetail />} />
|
<Route path="/recipes/:id" element={<ProtectedRoute><RecipeDetail /></ProtectedRoute>} />
|
||||||
<Route path="/recipes/new" element={<RecipeForm />} />
|
|
||||||
{/* <Route path="/favorites" element={<Favorites />} /> */}
|
|
||||||
|
|
||||||
{/* Autres pages */}
|
{/* Routes protégées */}
|
||||||
<Route path="/profile" element={<Profile />} />
|
<Route path="/recipes/new" element={<ProtectedRoute><RecipeForm /></ProtectedRoute>} />
|
||||||
<Route path="/" element={<Home />} />
|
<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>
|
</Routes>
|
||||||
</MainLayout>
|
</MainLayout>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
|
|||||||
@ -3,10 +3,11 @@ import axios from 'axios';
|
|||||||
|
|
||||||
export interface Recipe {
|
export interface Recipe {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title?: string;
|
||||||
ingredients: string;
|
instructions: string[];
|
||||||
generatedRecipe: string;
|
ingredients?: string;
|
||||||
createdAt: string;
|
generatedRecipe?: string;
|
||||||
|
createdAt?: string;
|
||||||
audioUrl?: 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 { cn } from "@/lib/utils"
|
||||||
import { motion, AnimatePresence } from "framer-motion"
|
import { motion, AnimatePresence } from "framer-motion"
|
||||||
import { Logo } from "@/components/illustrations/Logo"
|
import { Logo } from "@/components/illustrations/Logo"
|
||||||
|
import useAuth from "@/hooks/useAuth"
|
||||||
|
|
||||||
export function Header() {
|
export function Header() {
|
||||||
const [isAuthenticated, setIsAuthenticated] = useState(false)
|
const { isAuthenticated } = useAuth()
|
||||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false)
|
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false)
|
||||||
const location = useLocation()
|
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 = () => {
|
const toggleMobileMenu = () => {
|
||||||
setIsMobileMenuOpen(!isMobileMenuOpen)
|
setIsMobileMenuOpen(!isMobileMenuOpen)
|
||||||
@ -28,7 +21,6 @@ export function Header() {
|
|||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
localStorage.removeItem("token")
|
localStorage.removeItem("token")
|
||||||
setIsAuthenticated(false)
|
|
||||||
setIsMobileMenuOpen(false)
|
setIsMobileMenuOpen(false)
|
||||||
window.location.href = "/auth/login"
|
window.location.href = "/auth/login"
|
||||||
}
|
}
|
||||||
@ -36,9 +28,9 @@ export function Header() {
|
|||||||
const navItems = [
|
const navItems = [
|
||||||
{ name: "Accueil", path: "/", icon: Home, public: true },
|
{ name: "Accueil", path: "/", icon: Home, public: true },
|
||||||
{ name: "Recettes", path: "/recipes", icon: BookOpen, public: true },
|
{ name: "Recettes", path: "/recipes", icon: BookOpen, public: true },
|
||||||
// { name: "Mes recettes", path: "/my-recipes", icon: BookOpen, public: false },
|
{ name: "Mes recettes", path: "/recipes", icon: BookOpen, public: false },
|
||||||
// { name: "Favoris", path: "/favorites", icon: Heart, public: false },
|
{ name: "Favoris", path: "/favorites", icon: Heart, public: false },
|
||||||
// { name: "Profil", path: "/profile", icon: User, public: false },
|
{ name: "Profil", path: "/profile", icon: User, public: false },
|
||||||
]
|
]
|
||||||
|
|
||||||
const filteredNavItems = navItems.filter((item) => item.public || isAuthenticated)
|
const filteredNavItems = navItems.filter((item) => item.public || isAuthenticated)
|
||||||
@ -54,21 +46,25 @@ export function Header() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Navigation desktop */}
|
{/* Navigation desktop */}
|
||||||
<nav className="hidden md:flex items-center gap-10">
|
{
|
||||||
{filteredNavItems.map((item) => (
|
isAuthenticated && (
|
||||||
<Link
|
<nav className="hidden md:flex items-center gap-10">
|
||||||
key={item.path}
|
{filteredNavItems.map((item) => (
|
||||||
to={item.path}
|
<Link
|
||||||
className={cn(
|
key={item.path}
|
||||||
"text-sm font-medium transition-colors hover:text-orange-500 flex items-center gap-1.5",
|
to={item.path}
|
||||||
location.pathname === item.path ? "text-orange-500" : "text-muted-foreground",
|
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>
|
<item.icon className="h-4 w-4" />
|
||||||
))}
|
{item.name}
|
||||||
</nav>
|
</Link>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
{/* Boutons d'authentification */}
|
{/* Boutons d'authentification */}
|
||||||
<div className="hidden md:flex items-center gap-4">
|
<div className="hidden md:flex items-center gap-4">
|
||||||
@ -98,80 +94,84 @@ export function Header() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Bouton menu mobile */}
|
{/* Bouton menu mobile */}
|
||||||
<Button variant="ghost" size="icon" className="md:hidden rounded-full" onClick={toggleMobileMenu}>
|
{isAuthenticated && (
|
||||||
{isMobileMenuOpen ? <X className="h-6 w-6" /> : <Menu className="h-6 w-6" />}
|
<Button variant="ghost" size="icon" className="md:hidden rounded-full" onClick={toggleMobileMenu}>
|
||||||
</Button>
|
{isMobileMenuOpen ? <X className="h-6 w-6" /> : <Menu className="h-6 w-6" />}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Menu mobile */}
|
{/* Menu mobile */}
|
||||||
<AnimatePresence>
|
{isAuthenticated && (
|
||||||
{isMobileMenuOpen && (
|
<AnimatePresence>
|
||||||
<motion.div
|
{isMobileMenuOpen && (
|
||||||
className="md:hidden border-b"
|
<motion.div
|
||||||
initial={{ opacity: 0, height: 0 }}
|
className="md:hidden border-b"
|
||||||
animate={{ opacity: 1, height: "auto" }}
|
initial={{ opacity: 0, height: 0 }}
|
||||||
exit={{ opacity: 0, height: 0 }}
|
animate={{ opacity: 1, height: "auto" }}
|
||||||
transition={{ duration: 0.2 }}
|
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">
|
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-4 pb-6">
|
||||||
{filteredNavItems.map((item, index) => (
|
<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
|
<motion.div
|
||||||
key={item.path}
|
|
||||||
initial={{ opacity: 0, x: -10 }}
|
initial={{ opacity: 0, x: -10 }}
|
||||||
animate={{ opacity: 1, x: 0 }}
|
animate={{ opacity: 1, x: 0 }}
|
||||||
transition={{ delay: index * 0.05 }}
|
transition={{ delay: filteredNavItems.length * 0.05 }}
|
||||||
|
className="pt-2"
|
||||||
>
|
>
|
||||||
<Link
|
{isAuthenticated ? (
|
||||||
to={item.path}
|
<Button
|
||||||
className={cn(
|
variant="outline"
|
||||||
"flex items-center gap-3 py-2 text-sm font-medium transition-colors hover:text-orange-500",
|
onClick={handleLogout}
|
||||||
location.pathname === item.path ? "text-orange-500" : "text-muted-foreground",
|
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"
|
||||||
)}
|
>
|
||||||
onClick={() => setIsMobileMenuOpen(false)}
|
<LogOut className="mr-2 h-4 w-4" />
|
||||||
>
|
Déconnexion
|
||||||
<item.icon className="h-5 w-5" />
|
</Button>
|
||||||
{item.name}
|
) : (
|
||||||
</Link>
|
<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>
|
||||||
))}
|
</nav>
|
||||||
|
</div>
|
||||||
<motion.div
|
</motion.div>
|
||||||
initial={{ opacity: 0, x: -10 }}
|
)}
|
||||||
animate={{ opacity: 1, x: 0 }}
|
</AnimatePresence>
|
||||||
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>
|
|
||||||
</header>
|
</header>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,7 +29,7 @@ export function LoginForm({
|
|||||||
throw new Error("Échec de la connexion")
|
throw new Error("Échec de la connexion")
|
||||||
}
|
}
|
||||||
|
|
||||||
navigate("/")
|
navigate("/recipes")
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Erreur de connexion:", err);
|
console.error("Erreur de connexion:", err);
|
||||||
setError(err instanceof Error ? err.message : "Une erreur est survenue")
|
setError(err instanceof Error ? err.message : "Une erreur est survenue")
|
||||||
@ -52,7 +52,7 @@ export function LoginForm({
|
|||||||
|
|
||||||
localStorage.setItem("token", response.token)
|
localStorage.setItem("token", response.token)
|
||||||
|
|
||||||
navigate("/")
|
navigate("/recipes")
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Erreur de connexion Google:", err)
|
console.error("Erreur de connexion Google:", err)
|
||||||
setError(err instanceof Error ? err.message : "Une erreur est survenue")
|
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() {
|
export default function Home() {
|
||||||
return (
|
return (
|
||||||
<div className="md:space-y-24 space-y-4 pb-16">
|
<div className="md:space-y-24 space-y-4 pb-16">
|
||||||
222
|
|
||||||
<section className="py-12 md:py-20 flex justify-center">
|
<section className="py-12 md:py-20 flex justify-center">
|
||||||
<div className="container px-4 md:px-6">
|
<div className="container px-4 md:px-6">
|
||||||
<div className="grid gap-6 lg:grid-cols-2 lg:gap-12 items-center">
|
<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 !
|
secondes. Fini le gaspillage alimentaire !
|
||||||
</p>
|
</p>
|
||||||
<div className="flex flex-col gap-3 min-[400px]:flex-row">
|
<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">
|
<Button className="bg-gradient-to-r from-orange-500 to-amber-500 hover:from-orange-600 hover:to-amber-600 text-white">
|
||||||
Commencer maintenant
|
Commencer maintenant
|
||||||
<ArrowRight className="ml-2 h-4 w-4" />
|
<ArrowRight className="ml-2 h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<Link to="/auth/register">
|
{/* <Link to="/auth/register">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
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"
|
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
|
Créer un compte
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link> */}
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
<motion.div
|
<motion.div
|
||||||
@ -429,11 +428,11 @@ export default function Home() {
|
|||||||
<ArrowRight className="ml-2 h-4 w-4" />
|
<ArrowRight className="ml-2 h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<Link to="/recipes">
|
{/* <Link to="/recipes">
|
||||||
<Button variant="outline" className="border-white text-white hover:bg-white/20">
|
<Button variant="outline" className="border-white text-white hover:bg-white/20">
|
||||||
Explorer les recettes
|
Explorer les recettes
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link> */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|||||||
@ -11,8 +11,6 @@ import {
|
|||||||
Heart,
|
Heart,
|
||||||
Share2,
|
Share2,
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
Trash2,
|
|
||||||
Edit,
|
|
||||||
Printer,
|
Printer,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
Timer,
|
Timer,
|
||||||
@ -32,8 +30,8 @@ export default function RecipeDetail() {
|
|||||||
const [recipe, setRecipe] = useState<Recipe | null>(null)
|
const [recipe, setRecipe] = useState<Recipe | null>(null)
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [error, setError] = useState("")
|
const [error, setError] = useState("")
|
||||||
const [isFavorite, setIsFavorite] = useState(false)
|
// const [isFavorite, setIsFavorite] = useState(false)
|
||||||
const [addingToFavorites, setAddingToFavorites] = useState(false)
|
// const [addingToFavorites, setAddingToFavorites] = useState(false)
|
||||||
const [checkedIngredients, setCheckedIngredients] = useState<Set<number>>(new Set())
|
const [checkedIngredients, setCheckedIngredients] = useState<Set<number>>(new Set())
|
||||||
const [checkedSteps, setCheckedSteps] = useState<Set<number>>(new Set())
|
const [checkedSteps, setCheckedSteps] = useState<Set<number>>(new Set())
|
||||||
const [showScrollTop, setShowScrollTop] = useState(false)
|
const [showScrollTop, setShowScrollTop] = useState(false)
|
||||||
@ -68,8 +66,8 @@ export default function RecipeDetail() {
|
|||||||
|
|
||||||
// ✅ GET FAVORITE RECIPES & CHECK IF FAVORITE
|
// ✅ GET FAVORITE RECIPES & CHECK IF FAVORITE
|
||||||
try {
|
try {
|
||||||
const favorites = await recipeService.getFavoriteRecipes()
|
// const favorites = await recipeService.getFavoriteRecipes()
|
||||||
setIsFavorite(favorites.some((fav) => fav.id === id))
|
// setIsFavorite(favorites.some((fav) => fav.id === id))
|
||||||
} catch (favError) {
|
} catch (favError) {
|
||||||
// Ignorer les erreurs de favoris pour ne pas bloquer l'affichage
|
// Ignorer les erreurs de favoris pour ne pas bloquer l'affichage
|
||||||
console.log("Impossible de vérifier les favoris:", favError)
|
console.log("Impossible de vérifier les favoris:", favError)
|
||||||
@ -97,42 +95,42 @@ export default function RecipeDetail() {
|
|||||||
return () => window.removeEventListener("scroll", handleScroll)
|
return () => window.removeEventListener("scroll", handleScroll)
|
||||||
}, [id])
|
}, [id])
|
||||||
|
|
||||||
const handleToggleFavorite = async () => {
|
// const handleToggleFavorite = async () => {
|
||||||
if (!id || !recipe) return
|
// if (!id || !recipe) return
|
||||||
|
|
||||||
try {
|
// try {
|
||||||
setAddingToFavorites(true)
|
// setAddingToFavorites(true)
|
||||||
|
|
||||||
if (isFavorite) {
|
// if (isFavorite) {
|
||||||
// ✅ REMOVE FROM FAVORITES
|
// // ✅ REMOVE FROM FAVORITES
|
||||||
await recipeService.removeFromFavorites(id)
|
// await recipeService.removeFromFavorites(id)
|
||||||
setIsFavorite(false)
|
// setIsFavorite(false)
|
||||||
} else {
|
// } else {
|
||||||
// ✅ ADD TO FAVORITES
|
// // ✅ ADD TO FAVORITES
|
||||||
await recipeService.addToFavorites(id)
|
// await recipeService.addToFavorites(id)
|
||||||
setIsFavorite(true)
|
// setIsFavorite(true)
|
||||||
}
|
// }
|
||||||
} catch (err) {
|
// } catch (err) {
|
||||||
console.error("Erreur lors de la modification des favoris:", err)
|
// console.error("Erreur lors de la modification des favoris:", err)
|
||||||
// Ne pas afficher d'erreur à l'utilisateur pour cette fonctionnalité
|
// // Ne pas afficher d'erreur à l'utilisateur pour cette fonctionnalité
|
||||||
} finally {
|
// } finally {
|
||||||
setAddingToFavorites(false)
|
// setAddingToFavorites(false)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
const handleDeleteRecipe = async () => {
|
// const handleDeleteRecipe = async () => {
|
||||||
if (!id) return
|
// if (!id) return
|
||||||
if (!window.confirm("Êtes-vous sûr de vouloir supprimer cette recette ?")) return
|
// if (!window.confirm("Êtes-vous sûr de vouloir supprimer cette recette ?")) return
|
||||||
|
|
||||||
try {
|
// try {
|
||||||
// ✅ DELETE RECIPE
|
// // ✅ DELETE RECIPE
|
||||||
await recipeService.deleteRecipe(id)
|
// await recipeService.deleteRecipe(id)
|
||||||
navigate("/recipes")
|
// navigate("/recipes")
|
||||||
} catch (err) {
|
// } catch (err) {
|
||||||
console.error("Erreur lors de la suppression:", err)
|
// console.error("Erreur lors de la suppression:", err)
|
||||||
// Optionnel: afficher un message d'erreur à l'utilisateur
|
// // Optionnel: afficher un message d'erreur à l'utilisateur
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
const handleShare = () => {
|
const handleShare = () => {
|
||||||
if (!recipe) return
|
if (!recipe) return
|
||||||
@ -177,9 +175,6 @@ export default function RecipeDetail() {
|
|||||||
window.scrollTo({ top: 0, behavior: "smooth" })
|
window.scrollTo({ top: 0, behavior: "smooth" })
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========================================
|
|
||||||
// === LOADING & ERROR STATES ============
|
|
||||||
// ========================================
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <RecipeDetailLoader />
|
return <RecipeDetailLoader />
|
||||||
}
|
}
|
||||||
@ -222,7 +217,7 @@ export default function RecipeDetail() {
|
|||||||
<Button variant="ghost" size="icon" className="rounded-full" onClick={handleShare}>
|
<Button variant="ghost" size="icon" className="rounded-full" onClick={handleShare}>
|
||||||
<Share2 className="h-4 w-4" />
|
<Share2 className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
{/*
|
||||||
<Button
|
<Button
|
||||||
variant={isFavorite ? "default" : "ghost"}
|
variant={isFavorite ? "default" : "ghost"}
|
||||||
size="icon"
|
size="icon"
|
||||||
@ -231,7 +226,7 @@ export default function RecipeDetail() {
|
|||||||
disabled={addingToFavorites}
|
disabled={addingToFavorites}
|
||||||
>
|
>
|
||||||
<Heart className="h-4 w-4" />
|
<Heart className="h-4 w-4" />
|
||||||
</Button>
|
</Button> */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -371,41 +366,33 @@ export default function RecipeDetail() {
|
|||||||
{recipe.instructions && recipe.instructions.length > 0 ? (
|
{recipe.instructions && recipe.instructions.length > 0 ? (
|
||||||
recipe.instructions.map((instruction, index) => (
|
recipe.instructions.map((instruction, index) => (
|
||||||
<li key={index} className="flex">
|
<li key={index} className="flex">
|
||||||
<Checkbox
|
{/* <Checkbox
|
||||||
id={`step-${index}`}
|
id={`step-${index}`}
|
||||||
checked={checkedSteps.has(index)}
|
checked={checkedSteps.has(index)}
|
||||||
onCheckedChange={() => toggleStep(index)}
|
onCheckedChange={() => toggleStep(index)}
|
||||||
className="mr-3 mt-1 data-[state=checked]:bg-orange-500 data-[state=checked]:border-orange-500"
|
className="mr-3 mt-1 data-[state=checked]:bg-orange-500 data-[state=checked]:border-orange-500"
|
||||||
/>
|
/> */}
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<label
|
<p className={checkedSteps.has(index) ? "text-muted-foreground" : ""}>
|
||||||
htmlFor={`step-${index}`}
|
{instruction}
|
||||||
className={`flex items-center font-medium mb-2 cursor-pointer ${checkedSteps.has(index) ? "line-through text-muted-foreground" : ""}`}
|
{checkedSteps.has(index) && <CheckCircle2 className="ml-2 h-4 w-4 text-green-500 inline" />}
|
||||||
>
|
</p>
|
||||||
É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>
|
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<li className="flex">
|
<li className="flex">
|
||||||
<Checkbox
|
{/* <Checkbox
|
||||||
id="step-single"
|
id="step-single"
|
||||||
checked={checkedSteps.has(0)}
|
checked={checkedSteps.has(0)}
|
||||||
onCheckedChange={() => toggleStep(0)}
|
onCheckedChange={() => toggleStep(0)}
|
||||||
className="mr-3 mt-1 data-[state=checked]:bg-orange-500 data-[state=checked]:border-orange-500"
|
className="mr-3 mt-1 data-[state=checked]:bg-orange-500 data-[state=checked]:border-orange-500"
|
||||||
/>
|
/> */}
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<label
|
<p className={checkedSteps.has(0) ? "text-muted-foreground" : ""}>
|
||||||
htmlFor="step-single"
|
{recipe.generatedRecipe}
|
||||||
className={`flex items-center font-medium mb-2 cursor-pointer ${checkedSteps.has(0) ? "line-through text-muted-foreground" : ""}`}
|
{checkedSteps.has(0) && <CheckCircle2 className="ml-2 h-4 w-4 text-green-500 inline" />}
|
||||||
>
|
</p>
|
||||||
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>
|
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user