diff --git a/frontend/package.json b/frontend/package.json index f026693..b99e2c4 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@radix-ui/react-avatar": "^1.1.3", + "@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-label": "^2.1.2", "@radix-ui/react-select": "^2.1.6", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index b4d175b..9ce8e57 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@radix-ui/react-avatar': specifier: ^1.1.3 version: 1.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-checkbox': + specifier: ^1.1.4 + version: 1.1.4(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@radix-ui/react-dialog': specifier: ^1.1.6 version: 1.1.6(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -475,6 +478,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-checkbox@1.1.4': + resolution: {integrity: sha512-wP0CPAHq+P5I4INKe3hJrIa1WoNqqrejzW+zoU0rOvo1b9gDEJJFl2rYfO1PYJUQCc2H1WZxIJmyv9BS8i5fLw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-collection@1.1.2': resolution: {integrity: sha512-9z54IEKRxIa9VityapoEYMuByaG42iSy1ZXlY2KcuLSEtq8x4987/N6m15ppoMffgZX72gER2uHe1D9Y6Unlcw==} peerDependencies: @@ -2201,6 +2217,22 @@ snapshots: '@types/react': 19.0.10 '@types/react-dom': 19.0.4(@types/react@19.0.10) + '@radix-ui/react-checkbox@1.1.4(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.10)(react@19.0.0) + '@radix-ui/react-context': 1.1.1(@types/react@19.0.10)(react@19.0.0) + '@radix-ui/react-presence': 1.1.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@19.0.10)(react@19.0.0) + '@radix-ui/react-use-previous': 1.1.0(@types/react@19.0.10)(react@19.0.0) + '@radix-ui/react-use-size': 1.1.0(@types/react@19.0.10)(react@19.0.0) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + optionalDependencies: + '@types/react': 19.0.10 + '@types/react-dom': 19.0.4(@types/react@19.0.10) + '@radix-ui/react-collection@1.1.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.10)(react@19.0.0) diff --git a/frontend/src/assets/pasta-alla-vongole-home.jpeg b/frontend/src/assets/pasta-alla-vongole-home.jpeg new file mode 100644 index 0000000..3d0cff7 Binary files /dev/null and b/frontend/src/assets/pasta-alla-vongole-home.jpeg differ diff --git a/frontend/src/components/HowItWorksStep.tsx b/frontend/src/components/HowItWorksStep.tsx new file mode 100644 index 0000000..4b43ee5 --- /dev/null +++ b/frontend/src/components/HowItWorksStep.tsx @@ -0,0 +1,36 @@ +"use client" + +import { motion } from "framer-motion" +import type { LucideIcon } from "lucide-react" + +interface HowItWorksStepProps { + icon: LucideIcon + title: string + description: string + step: number + delay?: number +} + +export function HowItWorksStep({ icon: Icon, title, description, step, delay = 0 }: HowItWorksStepProps) { + return ( + +
+ {step} +
+
+
+ +

{title}

+
+

{description}

+
+
+ ) +} + diff --git a/frontend/src/components/PricingCard.tsx b/frontend/src/components/PricingCard.tsx new file mode 100644 index 0000000..b3edb89 --- /dev/null +++ b/frontend/src/components/PricingCard.tsx @@ -0,0 +1,85 @@ +import { Check } from "lucide-react" +import { Button } from "@/components/ui/button" +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card" +import { Badge } from "@/components/ui/badge" +import { Link } from "react-router-dom" + +interface PricingFeature { + text: string + included: boolean +} + +interface PricingCardProps { + name: string + description: string + price: string | null + duration?: string + features: PricingFeature[] + popular?: boolean + buttonText: string + buttonVariant?: "default" | "outline" + className?: string +} + +export function PricingCard({ + name, + description, + price, + duration = "mois", + features, + popular = false, + buttonText, + buttonVariant = "default", + className, +}: PricingCardProps) { + return ( + + {popular && ( + + Populaire + + )} + + + {name} + {description} + + + +
+ {price ? ( + <> + {price}€ + /{duration} + + ) : ( + Gratuit + )} +
+ + +
+ + + + + + +
+ ) +} + diff --git a/frontend/src/components/TestimonialCard.tsx b/frontend/src/components/TestimonialCard.tsx new file mode 100644 index 0000000..5bcfd01 --- /dev/null +++ b/frontend/src/components/TestimonialCard.tsx @@ -0,0 +1,44 @@ +import { Card, CardContent } from "@/components/ui/card" +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" +import { Star } from "lucide-react" + +interface TestimonialCardProps { + quote: string + author: string + role: string + avatarSrc?: string + rating: number +} + +export function TestimonialCard({ quote, author, role, avatarSrc, rating }: TestimonialCardProps) { + return ( + + +
+ {Array.from({ length: 5 }).map((_, i) => ( + + ))} +
+ +
"{quote}"
+ +
+ + + + {author + .split(" ") + .map((n) => n[0]) + .join("")} + + +
+

{author}

+

{role}

+
+
+
+
+ ) +} + diff --git a/frontend/src/components/header.tsx b/frontend/src/components/header.tsx index 4efa3f7..4a2d961 100644 --- a/frontend/src/components/header.tsx +++ b/frontend/src/components/header.tsx @@ -1,63 +1,73 @@ -import { useState, useEffect } from "react"; -import { Link, useLocation } from "react-router-dom"; -import { Button } from "@/components/ui/button"; -import { GalleryVerticalEnd, Menu, X } from "lucide-react"; -import { cn } from "@/lib/utils"; +"use client" + +import { useState, useEffect } from "react" +import { Link, useLocation } from "react-router-dom" +import { Button } from "@/components/ui/button" +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" export function Header() { - const [isAuthenticated, setIsAuthenticated] = useState(false); - const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); - const location = useLocation(); + const [isAuthenticated, setIsAuthenticated] = useState(false) + const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false) + const location = useLocation() useEffect(() => { // Vérifier si l'utilisateur est authentifié - const token = localStorage.getItem("token"); - setIsAuthenticated(!!token); - }, [location]); // Re-vérifier à chaque changement de route + const token = localStorage.getItem("token") + setIsAuthenticated(!!token) + + // Fermer le menu mobile lors d'un changement de route + setIsMobileMenuOpen(false) + }, [location]) const toggleMobileMenu = () => { - setIsMobileMenuOpen(!isMobileMenuOpen); - }; + setIsMobileMenuOpen(!isMobileMenuOpen) + } + + const handleLogout = () => { + localStorage.removeItem("token") + setIsAuthenticated(false) + setIsMobileMenuOpen(false) + window.location.href = "/auth/login" + } const navItems = [ - { name: "Accueil", path: "/", public: true }, - { name: "Recettes", path: "/recipes", public: true }, - { name: "Mes recettes", path: "/my-recipes", public: false }, - { name: "Favoris", path: "/favorites", public: false }, - { name: "Profil", path: "/profile", public: false }, - ]; + { 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 }, + ] + + const filteredNavItems = navItems.filter((item) => item.public || isAuthenticated) return ( -
+
- -
- -
- Freedge + +
{/* Navigation desktop */} {/* Boutons d'authentification */} @@ -65,91 +75,104 @@ export function Header() { {isAuthenticated ? ( ) : ( <> - + - + )}
{/* Bouton menu mobile */} -
{/* Menu mobile */} - {isMobileMenuOpen && ( -
-
-
- )} + {isAuthenticated ? ( + + ) : ( +
+ setIsMobileMenuOpen(false)}> + + + setIsMobileMenuOpen(false)}> + + +
+ )} + + +
+ + )} +
- ); -} \ No newline at end of file + ) +} + diff --git a/frontend/src/components/illustrations/KitchenIllustration.tsx b/frontend/src/components/illustrations/KitchenIllustration.tsx index 927a216..34ecf65 100644 --- a/frontend/src/components/illustrations/KitchenIllustration.tsx +++ b/frontend/src/components/illustrations/KitchenIllustration.tsx @@ -1,8 +1,23 @@ import { motion } from "framer-motion"; -export function KitchenIllustration() { +interface KitchenIllustrationProps { + height?: number; +} + +export function KitchenIllustration({ height = 200 }: KitchenIllustrationProps) { + // Calculate width based on original ratio (280:200) + const ratio = 280 / 200; + const width = height * ratio; + return ( - + {/* Pot */} diff --git a/frontend/src/components/illustrations/Logo.tsx b/frontend/src/components/illustrations/Logo.tsx new file mode 100644 index 0000000..86a5d88 --- /dev/null +++ b/frontend/src/components/illustrations/Logo.tsx @@ -0,0 +1,61 @@ +import { motion } from "framer-motion"; + +interface LogoProps { + size?: "sm" | "md" | "lg"; + showText?: boolean; +} + +export function Logo({ size = "md", showText = true }: LogoProps) { + const sizes = { + sm: { container: 24, icon: 18 }, + md: { container: 32, icon: 24 }, + lg: { container: 40, icon: 30 } + }; + + const containerSize = sizes[size].container; + const iconSize = sizes[size].icon; + + return ( +
+
+ + {/* Fridge SVG Icon */} + + + + + + + + + + +
+ + {showText && ( + + Freedge + + )} +
+ ); +} diff --git a/frontend/src/components/illustrations/RecipeDetailLoader.tsx b/frontend/src/components/illustrations/RecipeDetailLoader.tsx new file mode 100644 index 0000000..807bd3b --- /dev/null +++ b/frontend/src/components/illustrations/RecipeDetailLoader.tsx @@ -0,0 +1,72 @@ +import { motion } from "framer-motion"; + +export function RecipeDetailLoader() { + return ( +
+
+ {/* Pot */} +
+
+
+
+ + {/* Bubbling animation */} + + + + + + + {/* Spoon stirring animation */} + +
+
+
+

Préparation de votre recette...

+
+ ); +} diff --git a/frontend/src/components/login-form.tsx b/frontend/src/components/login-form.tsx index 2e797db..d0ff7f0 100644 --- a/frontend/src/components/login-form.tsx +++ b/frontend/src/components/login-form.tsx @@ -5,7 +5,6 @@ import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { login } from "@/api/auth" - export function LoginForm({ className, ...props @@ -40,9 +39,9 @@ export function LoginForm({ return (
-

Login to your account

+

Connexion à votre compte

- Enter your email below to login to your account + Entrez votre email ci-dessous pour vous connecter à votre compte

{error && ( @@ -56,7 +55,7 @@ export function LoginForm({ setEmail(e.target.value)} required @@ -64,12 +63,12 @@ export function LoginForm({
- + - Forgot your password? + Mot de passe oublié ?
- - Or continue with + + Ou continuer avec
- Don't have an account?{" "} + Vous n'avez pas de compte ?{" "} - Sign up + S'inscrire
diff --git a/frontend/src/components/ui/button.tsx b/frontend/src/components/ui/button.tsx index cd0857a..fd2e3a3 100644 --- a/frontend/src/components/ui/button.tsx +++ b/frontend/src/components/ui/button.tsx @@ -5,7 +5,7 @@ import { cva, type VariantProps } from "class-variance-authority" import { cn } from "@/lib/utils" const buttonVariants = cva( - "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-[color,box-shadow] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-[color,box-shadow] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive cursor-pointer", { variants: { variant: { @@ -14,7 +14,7 @@ const buttonVariants = cva( destructive: "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40", outline: - "border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground", + "border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground", secondary: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", ghost: "hover:bg-accent hover:text-accent-foreground", diff --git a/frontend/src/components/ui/checkbox.tsx b/frontend/src/components/ui/checkbox.tsx new file mode 100644 index 0000000..eac008b --- /dev/null +++ b/frontend/src/components/ui/checkbox.tsx @@ -0,0 +1,30 @@ +import * as React from "react" +import * as CheckboxPrimitive from "@radix-ui/react-checkbox" +import { CheckIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Checkbox({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + + + ) +} + +export { Checkbox } diff --git a/frontend/src/pages/Auth/Login.tsx b/frontend/src/pages/Auth/Login.tsx index 100f46e..893cc34 100644 --- a/frontend/src/pages/Auth/Login.tsx +++ b/frontend/src/pages/Auth/Login.tsx @@ -1,10 +1,11 @@ import { GalleryVerticalEnd } from "lucide-react" import { LoginForm } from "@/components/login-form" +import PastaVongole from "@/assets/pasta-alla-vongole-home.jpeg" export default function Login() { return (
-
+
@@ -21,7 +22,7 @@ export default function Login() {
Image diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index fb56d81..2927c33 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -1,90 +1,444 @@ -import { Link } from "react-router-dom"; -import { Button } from "@/components/ui/button"; -import { ArrowRight, ChefHat, Heart, Search } from "lucide-react"; +"use client" + +import { Link } from "react-router-dom" +import { Button } from "@/components/ui/button" +import { ArrowRight, Heart, Sparkles, Refrigerator, Camera, Mic, Clock, Utensils, Zap } from "lucide-react" +import { motion } from "framer-motion" +import { PricingCard } from "@/components/PricingCard" +import { TestimonialCard } from "@/components/TestimonialCard" +import { HowItWorksStep } from "@/components/HowItWorksStep" +import { KitchenIllustration } from "@/components/illustrations/KitchenIllustration" +import PastaVongole from "@/assets/pasta-alla-vongole-home.jpeg" export default function Home() { return ( -
- {/* Hero section */} -
+
+
-
-
-

- Découvrez et partagez des recettes délicieuses +
+ +
+ + + Propulsé par l'IA + +
+

+ Des recettes délicieuses avec ce que vous avez déjà

-

- Freedge vous aide à trouver des recettes adaptées à vos ingrédients disponibles et à partager vos créations culinaires avec la communauté. +

+ Freedge analyse les ingrédients de votre frigo et vous propose des recettes personnalisées en quelques + secondes. Fini le gaspillage alimentaire !

-
+
- - +
-
-
- Hero Image -
+
+ +
+
+ Recettes personnalisées +
+
+

Pasta Primavera

+
+ + 25 min +
+
+

+ Créé avec les ingrédients de votre frigo : pâtes, tomates, courgettes, basilic +

+
+
+ + Facile + + + Végétarien + +
+ +
+
+
+

- {/* Features section */} -
+ {/* AI Features section */} +
-
+

- Fonctionnalités principales + L'IA au service de votre cuisine

-

- Découvrez tout ce que Freedge peut faire pour vous +

+ Notre intelligence artificielle transforme votre façon de cuisiner

-
-
-
-
- + + +
+ +
+
+

Dites adieu au gaspillage alimentaire

+

+ Notre IA analyse les ingrédients que vous avez déjà et vous propose des recettes adaptées, vous + permettant d'utiliser ce que vous avez sous la main. +

+
+ +
+
+
+ +
+
+

Dictez vos ingrédients

+

+ Énumérez simplement ce que vous avez dans votre frigo +

+
+
+ +
+
+ +
+
+

Scannez votre frigo

+

+ Prenez une photo et notre IA identifie vos ingrédients +

+
+
+ +
+
+ +
+
+

Recettes personnalisées

+

+ Obtenez des suggestions adaptées à vos préférences et restrictions +

+
+
+
+ + + +
-

Recherche intelligente

-

- Trouvez des recettes en fonction des ingrédients que vous avez déjà chez vous. -

-
-
-
- + + + +
+
+
-

Créez et partagez

-

- Ajoutez vos propres recettes et partagez-les avec la communauté. -

-
-
-
- -
-

Favoris personnalisés

-

- Enregistrez vos recettes préférées pour y accéder rapidement. -

-
+
+ + {/* How it works section */} +
+
+ +
+

Comment ça marche

+

+ En trois étapes simples, transformez les ingrédients de votre frigo en délicieux repas +

+
+
+ +
+ + + + + +
+
+
+ + {/* Pricing section */} +
+
+ +
+

Plans d'abonnement

+

+ Choisissez le plan qui correspond à vos besoins culinaires +

+
+
+ +
+ + + + + + + + + + + +
+
+
+ + {/* Testimonials section */} +
+
+ +
+

+ Ce que disent nos utilisateurs +

+

+ Découvrez comment Freedge transforme la façon dont les gens cuisinent +

+
+
+ +
+ + + + + + + + + + + +
+
+
+ + {/* CTA section */} +
+
+ +
+ +
+

+ Prêt à transformer votre façon de cuisiner ? +

+

+ Rejoignez des milliers d'utilisateurs qui économisent du temps, de l'argent et réduisent leur gaspillage + alimentaire grâce à Freedge. +

+
+ + + + + + +
+
+ +
+
- ); -} \ No newline at end of file + ) +} + diff --git a/frontend/src/pages/Recipes/RecipeDetail.tsx b/frontend/src/pages/Recipes/RecipeDetail.tsx index 2a8d5e2..70f1e52 100644 --- a/frontend/src/pages/Recipes/RecipeDetail.tsx +++ b/frontend/src/pages/Recipes/RecipeDetail.tsx @@ -1,7 +1,9 @@ -import { useState, useEffect } from "react"; -import { useParams, useNavigate } from "react-router-dom"; -import { recipeService, Recipe } from "@/api/recipe"; -import { Button } from "@/components/ui/button"; +"use client" + +import { useState, useEffect, useRef } from "react" +import { useParams, useNavigate } from "react-router-dom" +import { recipeService, type Recipe } from "@/api/recipe" +import { Button } from "@/components/ui/button" import { Clock, Users, @@ -11,132 +13,175 @@ import { ArrowLeft, Trash2, Edit, - HeartOff -} from "lucide-react"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; + Printer, + CheckCircle2, + Timer, + ChevronUp, +} from "lucide-react" +import { motion, AnimatePresence } from "framer-motion" +import { RecipeDetailLoader } from "@/components/illustrations/RecipeDetailLoader" +import { Badge } from "@/components/ui/badge" +import { Card, CardContent } from "@/components/ui/card" +import { Checkbox } from "@/components/ui/checkbox" export default function RecipeDetail() { - const { id } = useParams<{ id: string }>(); - const navigate = useNavigate(); + const { id } = useParams<{ id: string }>() + const navigate = useNavigate() + const contentRef = useRef(null) - 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 [recipe, setRecipe] = useState(null) + const [loading, setLoading] = useState(true) + const [error, setError] = useState("") + 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) useEffect(() => { const fetchRecipeDetails = async () => { - if (!id) return; + if (!id) return try { - setLoading(true); + setLoading(true) // ✅ GET RECIPE DETAILS - const recipeData = await recipeService.getRecipeById(id); + const recipeData = await recipeService.getRecipeById(id) // Optionnel : conversion ingrédients / instructions si nécessaire - let ingredients = recipeData.ingredients; + let ingredients = recipeData.ingredients if (typeof ingredients === "string") { - ingredients = ingredients.split("\n").filter(item => item.trim() !== ""); + ingredients = ingredients.split("\n").filter((item) => item.trim() !== "") } // Correction: déclarer la variable instructions - let instructions: string[] = []; + let instructions: string[] = [] if (recipeData.generatedRecipe) { - instructions = recipeData.generatedRecipe - .split("\n") - .filter(item => item.trim() !== ""); + instructions = recipeData.generatedRecipe.split("\n").filter((item) => item.trim() !== "") } setRecipe({ ...recipeData, ingredients, instructions, // Ajouter instructions au recipe - }); + }) // ✅ 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); + console.log("Impossible de vérifier les favoris:", favError) } - } catch (err) { - console.error(err); - setError("Impossible de charger les détails de la recette"); + console.error(err) + setError("Impossible de charger les détails de la recette") } finally { - setLoading(false); + setLoading(false) } - }; + } - fetchRecipeDetails(); - }, [id]); + fetchRecipeDetails() + + // Setup scroll listener + const handleScroll = () => { + if (window.scrollY > 300) { + setShowScrollTop(true) + } else { + setShowScrollTop(false) + } + } + + window.addEventListener("scroll", handleScroll) + return () => window.removeEventListener("scroll", handleScroll) + }, [id]) const handleToggleFavorite = async () => { - if (!id || !recipe) return; + if (!id || !recipe) return try { - setAddingToFavorites(true); + setAddingToFavorites(true) if (isFavorite) { // ✅ REMOVE FROM FAVORITES - await recipeService.removeFromFavorites(id); - setIsFavorite(false); + await recipeService.removeFromFavorites(id) + setIsFavorite(false) } else { // ✅ ADD TO FAVORITES - await recipeService.addToFavorites(id); - setIsFavorite(true); + await recipeService.addToFavorites(id) + setIsFavorite(true) } - } 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é } finally { - setAddingToFavorites(false); + setAddingToFavorites(false) } - }; + } const handleDeleteRecipe = async () => { - if (!id) return; - if (!window.confirm("Êtes-vous sûr de vouloir supprimer cette recette ?")) return; + 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"); + await recipeService.deleteRecipe(id) + navigate("/recipes") } 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 } - }; + } const handleShare = () => { - if (!recipe) return; + if (!recipe) return if (navigator.share) { navigator.share({ title: recipe.title, text: recipe.description || "Découvrez cette recette !", url: window.location.href, - }); + }) } else { - navigator.clipboard.writeText(window.location.href); - alert("Lien copié dans le presse-papier !"); + navigator.clipboard.writeText(window.location.href) + alert("Lien copié dans le presse-papier !") } - }; + } + + const handlePrint = () => { + window.print() + } + + const toggleIngredient = (index: number) => { + const newChecked = new Set(checkedIngredients) + if (newChecked.has(index)) { + newChecked.delete(index) + } else { + newChecked.add(index) + } + setCheckedIngredients(newChecked) + } + + const toggleStep = (index: number) => { + const newChecked = new Set(checkedSteps) + if (newChecked.has(index)) { + newChecked.delete(index) + } else { + newChecked.add(index) + } + setCheckedSteps(newChecked) + } + + const scrollToTop = () => { + window.scrollTo({ top: 0, behavior: "smooth" }) + } // ======================================== // === LOADING & ERROR STATES ============ // ======================================== if (loading) { - return ( -
-
-
- ); + return } if (error || !recipe) { @@ -144,195 +189,268 @@ export default function RecipeDetail() {

Erreur

{error || "Recette introuvable"}

-
- ); + ) } + // Calculate total time + const totalTime = (recipe.preparationTime || 0) + (recipe.cookingTime || 0) + // ======================================== // === MAIN COMPONENT ===================== // ======================================== return ( -
-
- +
+ {/* Header */} +
+
+
+ -
- +
+ - + - - - -
-
- -
-
-
- {recipe.title} -
-
- -
-

{recipe.title}

- -

- {recipe.description || "Aucune description disponible"} -

- -
- {recipe.tags?.map((tag) => ( - - {tag} - - ))} -
- -
- {recipe.preparationTime && ( -
- - Préparation - {recipe.preparationTime} min -
- )} - - {recipe.cookingTime && ( -
- - Cuisson - {recipe.cookingTime} min -
- )} - - {recipe.servings && ( -
- - Portions - {recipe.servings} -
- )} - - {recipe.difficulty && ( -
- - Difficulté - {recipe.difficulty} -
- )} + + +
-
+
- - - Ingrédients - Instructions - - - -
-

Ingrédients

-
    - {Array.isArray(recipe.ingredients) ? ( - recipe.ingredients.map((ingredient, index) => ( -
  • - - {ingredient} -
  • - )) - ) : ( -
  • - - {recipe.ingredients} -
  • - )} -
+ {/* Main content */} +
+ + {/* Recipe Hero */} +
+ {recipe.title} +
+
+ {recipe.tags?.map((tag) => ( + + {tag} + + ))} +
+

{recipe.title}

+

{recipe.description || "Aucune description disponible"}

+
- - -
-

Instructions

-
    - {recipe.instructions && recipe.instructions.length > 0 ? ( - recipe.instructions.map((instruction, index) => ( -
  1. - - {index + 1} + {/* Recipe Info Cards */} +
    + + + + Préparation + {recipe.preparationTime || 0} min + + + + + + + Cuisson + {recipe.cookingTime || 0} min + + + + + + + Portions + {recipe.servings || "-"} + + + + + + + Difficulté + {recipe.difficulty || "-"} + + +
    + + {/* Recipe Content */} +
    + {/* Ingredients */} +
    + + +

    + + 1 -

    {instruction}

    -

  2. - )) - ) : ( -
  3. - - 1 - -

    {recipe.generatedRecipe}

    -
  4. - )} -
+ Ingrédients + + + {totalTime > 0 && ( +
+

+ + Temps total: {totalTime} min +

+
+ )} + +
    + {Array.isArray(recipe.ingredients) ? ( + recipe.ingredients.map((ingredient, index) => ( +
  • + toggleIngredient(index)} + className="mr-2 data-[state=checked]:bg-orange-500 data-[state=checked]:border-orange-500" + /> + +
  • + )) + ) : ( +
  • + toggleIngredient(0)} + className="mr-2 data-[state=checked]:bg-orange-500 data-[state=checked]:border-orange-500" + /> + +
  • + )} +
+ + +
+ + {/* Instructions */} +
+ + +

+ + 2 + + Instructions +

+ +
    + {recipe.instructions && recipe.instructions.length > 0 ? ( + recipe.instructions.map((instruction, index) => ( +
  1. + toggleStep(index)} + className="mr-3 mt-1 data-[state=checked]:bg-orange-500 data-[state=checked]:border-orange-500" + /> +
    + +

    {instruction}

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

    {recipe.generatedRecipe}

    +
    +
  4. + )} +
+
+
+
-
-
+ + {/* Admin Actions */} +
+ + + +
+ + + + {/* Scroll to top button */} + + {showScrollTop && ( + + + + )} +
- ); + ) } + diff --git a/frontend/src/pages/Recipes/RecipeForm.tsx b/frontend/src/pages/Recipes/RecipeForm.tsx index ae0674a..b74f981 100644 --- a/frontend/src/pages/Recipes/RecipeForm.tsx +++ b/frontend/src/pages/Recipes/RecipeForm.tsx @@ -99,8 +99,8 @@ export default function RecipeForm() { } return ( -
-
+
+
-
+
{/* Illustrations */} -
- +
+ +

+ Notre intelligence artificielle transforme votre façon de cuisiner +

- - Créer une nouvelle recette - + + Créer une nouvelle recette + Enregistrez ou téléchargez un fichier audio listant les ingrédients disponibles, et nous générerons une recette pour vous. - + {error && (
{error} @@ -151,34 +156,6 @@ export default function RecipeForm() {
)} -
- - - - -
-

Enregistrez-vous en listant les ingrédients que vous avez à disposition. Notre IA générera une recette adaptée à ces ingrédients. @@ -197,7 +174,7 @@ export default function RecipeForm() { )} - + + {!audioFile && !isRecording && recordingStatus !== "processing" && !loading && ( + + )} + + {isRecording && ( + + )} + + {audioFile && !isRecording && recordingStatus !== "processing" && !loading && ( + + )} + + {loading && ( + + )}

- - {/* Recording button at bottom */} - {recordingStatus !== "processing" && !loading && !audioFile && ( -
- - - -
- )} -
) } diff --git a/frontend/src/pages/Recipes/RecipeList.tsx b/frontend/src/pages/Recipes/RecipeList.tsx index e98ae44..68c1679 100644 --- a/frontend/src/pages/Recipes/RecipeList.tsx +++ b/frontend/src/pages/Recipes/RecipeList.tsx @@ -89,7 +89,7 @@ export default function RecipeList() { )} -
+
Toutes @@ -116,7 +116,7 @@ export default function RecipeList() { ) : (