From 62d65c4c6c442fe65cd0653c38b7a8e1ca9f08b5 Mon Sep 17 00:00:00 2001 From: GARATONCODE Date: Thu, 5 Feb 2026 14:35:15 +0100 Subject: [PATCH 01/12] feat: implement header visibility based on route in App component --- src/App.tsx | 90 +++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 80 insertions(+), 10 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 074a653..2159660 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,6 +3,7 @@ import { Route, BrowserRouter as Router, Routes, + useLocation, } from "react-router-dom"; import Header from "./components/Header"; import ProtectedRoute from "./components/ProtectedRoute"; @@ -16,10 +17,14 @@ import Settings from "./pages/Settings/page"; import ShoppingList from "./pages/ShoppingList/page"; import Signup from "./pages/Signup/page"; -function App() { +const AppContent = () => { + const location = useLocation(); + const hideHeader = + location.pathname === "/login" || location.pathname === "/signup"; + return ( - -
+ <> + {!hideHeader &&
} } /> } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> } /> + + ); +}; + +function App() { + return ( + + ); } From fe2cd95eb4b275dde24e493eda45908d8eca9578 Mon Sep 17 00:00:00 2001 From: GARATONCODE Date: Thu, 5 Feb 2026 14:37:04 +0100 Subject: [PATCH 02/12] feat: add width and height attributes to user icon SVG --- src/assets/user-icon.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/assets/user-icon.svg b/src/assets/user-icon.svg index 9900824..a133b88 100644 --- a/src/assets/user-icon.svg +++ b/src/assets/user-icon.svg @@ -1,4 +1,4 @@ - + From 6123434ebee575ed42eb3cba69d06fd98d9fd5d2 Mon Sep 17 00:00:00 2001 From: GARATONCODE Date: Thu, 5 Feb 2026 14:37:12 +0100 Subject: [PATCH 03/12] feat: refactor user icon styles and navigation alignment in Header.css --- src/components/Header.css | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/components/Header.css b/src/components/Header.css index f4a2e76..f119eeb 100644 --- a/src/components/Header.css +++ b/src/components/Header.css @@ -76,23 +76,32 @@ width: 100%; } -/* User icon */ -.user-link { +/* Navigation items alignment */ +.navigation li { display: flex; align-items: center; } -.user-link::after { +/* User icon */ +.navigation a.user-link { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0; +} + +.navigation a.user-link::after { display: none; } -.user-link .user-icon { - width: 28px; - height: 28px; +.user-icon { + width: 1rem; + height: 1rem; + vertical-align: middle; transition: transform 0.2s ease; } -.user-link:hover .user-icon { +.navigation a.user-link:hover .user-icon { transform: scale(1.1); } From 4a41e4626dcb0c1fda77e856de53a869b0d021d8 Mon Sep 17 00:00:00 2001 From: GARATONCODE Date: Thu, 5 Feb 2026 14:37:18 +0100 Subject: [PATCH 04/12] feat: add width and height styles to user icon in Header component --- src/components/Header.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 97b8346..a7fdf7f 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -31,7 +31,12 @@ const Header: React.FC = ({ logoUrl = "./public/logo.png" }) => {
  • - Mon compte + Mon compte
  • From 1fcb8673cdbb80f138b4ec14bdf53bfe0ecc634b Mon Sep 17 00:00:00 2001 From: GARATONCODE Date: Thu, 5 Feb 2026 14:37:24 +0100 Subject: [PATCH 05/12] feat: implement custom recipes fetching and filtering in Catalogue component --- src/pages/Catalogue/page.tsx | 267 +++++++++++++++++++++++++++++++---- 1 file changed, 241 insertions(+), 26 deletions(-) diff --git a/src/pages/Catalogue/page.tsx b/src/pages/Catalogue/page.tsx index 4b84f2b..d84d67d 100644 --- a/src/pages/Catalogue/page.tsx +++ b/src/pages/Catalogue/page.tsx @@ -1,9 +1,15 @@ +import { useEffect, useMemo, useState } from "react"; import { Link } from "react-router-dom"; import recipesData from "../../data/recipes.json"; +const API_BASE_URL = import.meta.env.PROD + ? "https://7pret-production.up.railway.app" + : "http://localhost:5173"; + interface RecipeProps { id: number; name: string; + type: string; cuisine: string; Time: number; image: string; @@ -11,49 +17,258 @@ interface RecipeProps { servings: number; } +interface CustomRecipe { + id: string; + name: string; + type: string | null; + cuisine: string | null; + prepTime: number | null; + cookTime: number | null; + image: string | null; + difficulty: string | null; + servings: number | null; +} + const Catalogue = () => { const recipes: RecipeProps[] = recipesData as unknown as RecipeProps[]; + const [customRecipes, setCustomRecipes] = useState([]); + const [filterCuisine, setFilterCuisine] = useState(""); + const [filterType, setFilterType] = useState(""); + + useEffect(() => { + const fetchCustomRecipes = async () => { + try { + const response = await fetch(`${API_BASE_URL}/api/custom-recipes`, { + credentials: "include", + }); + if (response.ok) { + const data = await response.json(); + setCustomRecipes(data); + } + } catch (error) { + console.error( + "Erreur lors du chargement des recettes personnalisées:", + error, + ); + } + }; + fetchCustomRecipes(); + }, []); + + const uniqueCuisines = useMemo(() => { + const cuisines = new Set(); + for (const recipe of recipes) { + if (recipe.cuisine) cuisines.add(recipe.cuisine); + } + for (const recipe of customRecipes) { + if (recipe.cuisine) cuisines.add(recipe.cuisine); + } + return Array.from(cuisines).sort(); + }, [customRecipes]); + + const uniqueTypes = useMemo(() => { + const types = new Set(); + for (const recipe of recipes) { + if (recipe.type) types.add(recipe.type); + } + for (const recipe of customRecipes) { + if (recipe.type) types.add(recipe.type); + } + return Array.from(types).sort(); + }, [customRecipes]); + + const filteredRecipes = useMemo(() => { + return recipes.filter((recipe) => { + const matchCuisine = !filterCuisine || recipe.cuisine === filterCuisine; + const matchType = !filterType || recipe.type === filterType; + return matchCuisine && matchType; + }); + }, [filterCuisine, filterType]); + + const filteredCustomRecipes = useMemo(() => { + return customRecipes.filter((recipe) => { + const matchCuisine = !filterCuisine || recipe.cuisine === filterCuisine; + const matchType = !filterType || recipe.type === filterType; + return matchCuisine && matchType; + }); + }, [customRecipes, filterCuisine, filterType]); return (
    -

    Bievenue sur le Catalogue

    +

    Bienvenue sur le Catalogue

    Voici la liste de nos recettes :

    - {recipes.map((recipe) => ( - + + +
    + +
    + + +
    + + {(filterCuisine || filterType) && ( + + )}
    + + {filteredCustomRecipes.length > 0 && ( + <> +

    Mes recettes personnalisées

    +
    + {filteredCustomRecipes.map((recipe) => ( + +

    {recipe.name}

    +

    + {recipe.cuisine || "Non spécifié"} + {recipe.type && ` - ${recipe.type}`} +

    +

    + {(recipe.prepTime || 0) + (recipe.cookTime || 0)} min +           +           +           +       Pers: {recipe.servings || "?"} +

    + {recipe.image && ( + {recipe.name} + )} + + ))} +
    + + )} + +

    Recettes proposées

    + {filteredRecipes.length === 0 ? ( +

    + Aucune recette ne correspond aux filtres sélectionnés. +

    + ) : ( +
    + {filteredRecipes.map((recipe) => ( + +

    {recipe.name}

    +

    + {recipe.cuisine} + {recipe.type && ` - ${recipe.type}`} +

    +

    + {recipe.Time} min +           +           +           +       Pers: {recipe.servings} +

    + {`Difficulté: + + ))} +
    + )} ); }; From 491b059c4f2d0526544607337ac64c1832ab9555 Mon Sep 17 00:00:00 2001 From: GARATONCODE Date: Thu, 5 Feb 2026 14:37:32 +0100 Subject: [PATCH 06/12] feat: enhance RecipeDetail component with editing and deletion functionality --- src/pages/Catalogue/recipedetail.tsx | 453 +++++++++++++++++++++------ 1 file changed, 364 insertions(+), 89 deletions(-) diff --git a/src/pages/Catalogue/recipedetail.tsx b/src/pages/Catalogue/recipedetail.tsx index bb9192d..c558d45 100644 --- a/src/pages/Catalogue/recipedetail.tsx +++ b/src/pages/Catalogue/recipedetail.tsx @@ -3,14 +3,18 @@ import { useNavigate, useParams } from "react-router-dom"; import recipesData from "../../data/recipes.json"; import "./recipedetail.css"; +const API_BASE_URL = import.meta.env.PROD + ? "https://7pret-production.up.railway.app" + : "http://localhost:5173"; + interface Ingredient { name: string; quantity: string | number; unit: string; } -interface RecipeDetailProps { - id: number; +interface Recipe { + id: number | string; name: string; type?: string; cuisine?: string; @@ -24,44 +28,143 @@ interface RecipeDetailProps { rating?: number; } -const RecipeDetail = () => { +interface RecipeDetailComponentProps { + isCustom?: boolean; +} + +const RecipeDetail = ({ isCustom = false }: RecipeDetailComponentProps) => { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); - const [recipe, setRecipe] = useState(null); + const [recipe, setRecipe] = useState(null); const [error, setError] = useState(""); + const [loading, setLoading] = useState(true); + const [isEditing, setIsEditing] = useState(false); + const [editedRecipe, setEditedRecipe] = useState(null); + const [saving, setSaving] = useState(false); const handleAddToShoppingList = () => { if (!recipe) return; const currentPanier = JSON.parse(localStorage.getItem("panier") || "[]"); if (recipe.ingredients) { - currentPanier.push(...recipe.ingredients); + const recipeGroup = { + recipeId: recipe.id, + recipeName: recipe.name, + originalServings: recipe.servings || 1, + currentServings: recipe.servings || 1, + ingredients: recipe.ingredients, + }; + currentPanier.push(recipeGroup); } localStorage.setItem("panier", JSON.stringify(currentPanier)); navigate("/ShoppingList"); }; - useEffect(() => { + const handleEdit = () => { + setEditedRecipe(recipe); + setIsEditing(true); + }; + + const handleCancelEdit = () => { + setEditedRecipe(null); + setIsEditing(false); + }; + + const handleSave = async () => { + if (!editedRecipe) return; + setSaving(true); try { - const recipes = ( - Array.isArray(recipesData) - ? recipesData - : (recipesData as { recipes: RecipeDetailProps[] }).recipes - ) as RecipeDetailProps[]; - - if (!recipes) { - setError("Impossible de charger les données des recettes."); - return; + const response = await fetch(`${API_BASE_URL}/api/custom-recipes/${id}`, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + credentials: "include", + body: JSON.stringify({ + name: editedRecipe.name, + cuisine: editedRecipe.cuisine, + difficulty: editedRecipe.difficulty, + prepTime: editedRecipe.prepTime, + cookTime: editedRecipe.cookTime, + servings: editedRecipe.servings, + image: editedRecipe.image, + ingredients: editedRecipe.ingredients, + steps: editedRecipe.steps, + }), + }); + if (response.ok) { + const updated = await response.json(); + setRecipe(updated); + setIsEditing(false); + setEditedRecipe(null); + } else { + setError("Erreur lors de la sauvegarde."); } + } catch (err) { + console.error(err); + setError("Erreur de connexion."); + } finally { + setSaving(false); + } + }; - const found = recipes.find((r) => r.id === Number(id)); - setRecipe(found || null); + const handleDelete = async () => { + if (!confirm("Supprimer cette recette ?")) return; + try { + const response = await fetch(`${API_BASE_URL}/api/custom-recipes/${id}`, { + method: "DELETE", + credentials: "include", + }); + if (response.ok) { + navigate("/Catalogue"); + } else { + setError("Erreur lors de la suppression."); + } } catch (err) { console.error(err); - setError("Une erreur est survenue lors du chargement."); + setError("Erreur de connexion."); } - }, [id]); + }; + + useEffect(() => { + const fetchRecipe = async () => { + setLoading(true); + try { + if (isCustom) { + const response = await fetch( + `${API_BASE_URL}/api/custom-recipes/${id}`, + { credentials: "include" }, + ); + if (response.ok) { + const data = await response.json(); + setRecipe(data); + } else { + setError("Recette personnalisée introuvable."); + } + } else { + const recipes = ( + Array.isArray(recipesData) + ? recipesData + : (recipesData as { recipes: Recipe[] }).recipes + ) as Recipe[]; + + if (!recipes) { + setError("Impossible de charger les données des recettes."); + return; + } + + const found = recipes.find((r) => r.id === Number(id)); + setRecipe(found || null); + } + } catch (err) { + console.error(err); + setError("Une erreur est survenue lors du chargement."); + } finally { + setLoading(false); + } + }; + + fetchRecipe(); + }, [id, isCustom]); if (error) return
    {error}
    ; if (!recipe) @@ -80,82 +183,254 @@ const RecipeDetail = () => { ← Retour au catalogue - - -

    Détails de la recette

    - -
    -
    - {recipe.name} +
    + + + {isCustom && !isEditing && ( + <> + + + + )} +
    + +

    + {isEditing ? "Modifier la recette" : "Détails de la recette"} +

    + + {isEditing && editedRecipe ? ( +
    + + + +
    + + + +
    + +
    + + +
    -
    -

    {recipe.name}

    -
    - - {recipe.cuisine || "Non classé"} - - - {recipe.difficulty || "-"} - - {recipe.rating && ( - ★ {recipe.rating} - )} + ) : ( +
    +
    + {recipe.name}
    - -
    -
    -

    Préparation

    -

    {recipe.prepTime || 0} min

    +
    +

    {recipe.name}

    +
    + + {recipe.cuisine || "Non classé"} + + + {recipe.difficulty || "-"} + + {recipe.rating && ( + ★ {recipe.rating} + )}
    -
    -

    Cuisson

    -

    {recipe.cookTime || 0} min

    -
    -
    -

    Portions

    -

    {recipe.servings || 0}

    + +
    +
    +

    Préparation

    +

    {recipe.prepTime || 0} min

    +
    +
    +

    Cuisson

    +

    {recipe.cookTime || 0} min

    +
    +
    +

    Portions

    +

    {recipe.servings || 0}

    +
    -
    -

    Ingrédients

    -
      - {ingredients.length > 0 ? ( - ingredients.map((ingredient, index) => ( -
    • - {ingredient.name} : {ingredient.quantity} {ingredient.unit} -
    • - )) - ) : ( -
    • Aucun ingrédient listé.
    • - )} -
    - -

    Instructions

    -
      - {Array.isArray(steps) && steps.length > 0 ? ( - steps.map((step) => ( -
    • - {step} -
    • - )) - ) : typeof steps === "string" ? ( -
    • {steps}
    • - ) : ( -
    • Aucune instruction disponible.
    • - )} -
    +

    Ingrédients

    +
      + {ingredients.length > 0 ? ( + ingredients.map((ingredient, index) => ( +
    • + {ingredient.name} : {ingredient.quantity} {ingredient.unit} +
    • + )) + ) : ( +
    • Aucun ingrédient listé.
    • + )} +
    + +

    Instructions

    +
      + {Array.isArray(steps) && steps.length > 0 ? ( + steps.map((step) => ( +
    • + {step} +
    • + )) + ) : typeof steps === "string" ? ( +
    • {steps}
    • + ) : ( +
    • Aucune instruction disponible.
    • + )} +
    +
    -
    + )}
    ); }; From 243c7e597917b3bebc78e57afa24ad5e181ec3c3 Mon Sep 17 00:00:00 2001 From: GARATONCODE Date: Thu, 5 Feb 2026 14:37:37 +0100 Subject: [PATCH 07/12] feat: add CreateRecipe.css for styling the recipe creation form --- src/pages/CreateRecipe/CreateRecipe.css | 104 ++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 src/pages/CreateRecipe/CreateRecipe.css diff --git a/src/pages/CreateRecipe/CreateRecipe.css b/src/pages/CreateRecipe/CreateRecipe.css new file mode 100644 index 0000000..445bc64 --- /dev/null +++ b/src/pages/CreateRecipe/CreateRecipe.css @@ -0,0 +1,104 @@ +.create-recipe-form { + padding: 20px; + max-width: 600px; + margin: 0 auto; + display: flex; + flex-direction: column; + gap: 10px; +} + +.create-recipe-title { + margin: 0 0 10px; +} + +.create-recipe-section { + border: 1px solid #ccc; + padding: 15px; + border-radius: 8px; + margin-bottom: 15px; +} + +.create-recipe-section-title { + margin: 0 0 10px; +} + +.create-recipe-label { + display: block; + margin-bottom: 8px; +} + +.create-recipe-input { + padding: 8px; + margin: 5px 0; + width: 100%; + box-sizing: border-box; +} + +.create-recipe-row { + display: flex; + gap: 5px; + margin-bottom: 5px; + align-items: flex-start; +} + +.create-recipe-input-name { + flex: 2; +} + +.create-recipe-input-qty, +.create-recipe-input-unit { + flex: 1; +} + +.create-recipe-textarea { + width: 100%; + min-height: 60px; + padding: 8px; + box-sizing: border-box; +} + +.create-recipe-step-index { + font-weight: bold; + line-height: 2; +} + +.create-recipe-button { + padding: 8px 12px; + border: none; + cursor: pointer; + border-radius: 4px; +} + +.create-recipe-button-primary { + padding: 15px; + font-size: 1.1rem; + background: #4caf50; + color: white; +} + +.create-recipe-button-secondary { + margin-top: 10px; + background: #e0e0e0; +} + +.create-recipe-button-danger { + background: red; + color: white; +} + +.create-recipe-button-fit { + height: fit-content; +} + +.create-recipe-message { + margin-top: 20px; + padding: 10px; +} + +.create-recipe-message-success { + background: #dff0d8; +} + +.create-recipe-message-error { + background: #f2dede; +} From b2054a04c34f95cc5b3008591316bded67a1670a Mon Sep 17 00:00:00 2001 From: GARATONCODE Date: Thu, 5 Feb 2026 14:37:42 +0100 Subject: [PATCH 08/12] feat: implement CreateRecipe component with form handling and API integration --- src/pages/CreateRecipe/page.tsx | 294 +++++++++++++++++++++++++++++++- 1 file changed, 290 insertions(+), 4 deletions(-) diff --git a/src/pages/CreateRecipe/page.tsx b/src/pages/CreateRecipe/page.tsx index 716d9b8..e847989 100644 --- a/src/pages/CreateRecipe/page.tsx +++ b/src/pages/CreateRecipe/page.tsx @@ -1,9 +1,295 @@ +import { useState } from "react"; +import "./CreateRecipe.css"; + +const API_BASE_URL = import.meta.env.PROD + ? "https://7pret-production.up.railway.app" + : "http://localhost:5173"; + const CreateRecipe = () => { + const [name, setName] = useState(""); + const [type, setType] = useState(""); + const [cuisine, setCuisine] = useState(""); + const [difficulty, setDifficulty] = useState(""); + const [prepTime, setPreptime] = useState(0); + const [cookTime, setCookTime] = useState(0); + const [servings, setServings] = useState(0); + const [image, setImage] = useState(""); + + const [ingredients, setIngredients] = useState([ + { id: crypto.randomUUID(), name: "", quantity: 0, unit: "" }, + ]); + const [steps, setSteps] = useState([{ id: crypto.randomUUID(), text: "" }]); + + const [loading, setLoading] = useState(false); + const [message, setMessage] = useState(""); + + const updateIngredient = ( + index: number, + field: string, + value: string | number, + ) => { + const newIngredients = [...ingredients]; + // @ts-expect-error - On sait que le champ existe + newIngredients[index][field] = value; + setIngredients(newIngredients); + }; + + const addIngredient = () => { + setIngredients([ + ...ingredients, + { id: crypto.randomUUID(), name: "", quantity: 0, unit: "" }, + ]); + }; + + const removeIngredient = (index: number) => { + setIngredients(ingredients.filter((_, i) => i !== index)); + }; + + const updateStep = (id: string, value: string) => { + const newSteps = steps.map((step) => + step.id === id ? { ...step, text: value } : step, + ); + setSteps(newSteps); + }; + + const addStep = () => { + setSteps([...steps, { id: crypto.randomUUID(), text: "" }]); + }; + + const removeStep = (id: string) => { + setSteps(steps.filter((step) => step.id !== id)); + }; + + const handleSubmit = async (e: React.SyntheticEvent) => { + e.preventDefault(); + setLoading(true); + setMessage(""); + + try { + const response = await fetch(`${API_BASE_URL}/api/custom-recipes`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + credentials: "include", + body: JSON.stringify({ + name: name, + type: type, + cuisine: cuisine, + difficulty: difficulty, + prepTime: Number(prepTime), + cookTime: Number(cookTime), + servings: Number(servings), + image: image, + ingredients: ingredients.filter((i) => i.name.trim() !== ""), + steps: steps.filter((s) => s.text.trim() !== "").map((s) => s.text), + }), + }); + + if (response.ok) { + setMessage("✅ Recette créée avec succès !"); + setName(""); + setType(""); + setCuisine(""); + setDifficulty(""); + setPreptime(0); + setCookTime(0); + setServings(0); + setImage(""); + setIngredients([ + { id: crypto.randomUUID(), name: "", quantity: 0, unit: "" }, + ]); + setSteps([{ id: crypto.randomUUID(), text: "" }]); + } else { + const errorData = await response.json(); + console.error("Erreur API:", errorData); + setMessage( + "❌ Erreur : " + + (errorData.message || "Non autorisé ou données invalides"), + ); + } + } catch (error) { + console.error(error); + setMessage("❌ Erreur de connexion au serveur."); + } finally { + setLoading(false); + } + }; + return ( -
    -

    Bienvenue sur Create Recipe

    -

    Le contenu de votre page s'affiche ici.

    -
    +
    +

    Créer une recette personnalisée

    + +
    +

    Informations générales

    + setName(e.target.value)} + required + /> + setType(e.target.value)} + /> + setCuisine(e.target.value)} + /> + setDifficulty(e.target.value)} + /> + setImage(e.target.value)} + /> +
    + +
    +

    Détails

    + + + +
    + +
    +

    Ingrédients

    + {ingredients.map((ing, index) => ( +
    + updateIngredient(index, "name", e.target.value)} + /> + + updateIngredient(index, "quantity", Number(e.target.value)) + } + /> + updateIngredient(index, "unit", e.target.value)} + /> + {ingredients.length > 1 && ( + + )} +
    + ))} + +
    + +
    +

    Étapes

    + {steps.map((step, index) => ( +
    + {index + 1}. +