From 568606213d1fb7afcdba8d28bac256376086b1ac Mon Sep 17 00:00:00 2001 From: Anika Raemer Date: Fri, 12 Sep 2025 19:25:50 +0200 Subject: [PATCH] Load and save recipes from backend --- backend/src/server.ts | 19 ++++++-- frontend/src/api/recipePoint.ts | 30 +++++++++++++ .../components/recipes/RecipeDetailPage.tsx | 43 ++++++++++++++----- .../src/components/recipes/RecipeEditPage.tsx | 1 + .../src/components/recipes/RecipeListPage.tsx | 25 ++++++++++- 5 files changed, 102 insertions(+), 16 deletions(-) diff --git a/backend/src/server.ts b/backend/src/server.ts index 9208641..f6a2b74 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -12,22 +12,35 @@ app.use(express.json()); let recipeList = recipes; // Routes -app.get("/recipe", (req, res) => res.json(recipeList)); +app.get("/recipe", (req, res) => { + console.log("GET /recipe") + res.json(recipeList); +}); app.get("/recipe/:id", (req, res) => { + let recipeId : string = req.params.id; + console.log("GET /recipe/", recipeId); const recipe = recipeList.find(r => r.id === req.params.id); + console.log(recipe ? "SUCCESS" : "404") recipe ? res.json(recipe) : res.status(404).send("Recipe not found"); }); app.post("/recipe", (req, res) => { + console.log("POST /recipe") const newRecipe: Recipe = { id: uuidv4(), ...req.body }; recipeList.push(newRecipe); res.status(201).json(newRecipe); }); app.put("/recipe/:id", (req, res) => { - const index = recipes.findIndex(r => r.id === req.params.id); - if (index === -1) return res.status(404).send("Recipe not found"); + let recipeId : string = req.params.id; + console.log("PUT /recipe/", recipeId) + const index = recipes.findIndex(r => r.id === recipeId); + if (index === -1) { + console.log("404") + return res.status(404).send("Recipe not found"); + } + console.log("SUCCESS"); recipeList[index] = { ...recipeList[index], ...req.body }; res.json(recipeList[index]); }); diff --git a/frontend/src/api/recipePoint.ts b/frontend/src/api/recipePoint.ts index f2916a7..9b0f458 100644 --- a/frontend/src/api/recipePoint.ts +++ b/frontend/src/api/recipePoint.ts @@ -4,8 +4,16 @@ import { API_BASE_URL } from "../config/api" /** * Util for handling the recipe api */ +/** + * URL for handling recipes + */ const RECIPE_URL = `${API_BASE_URL}/recipe` +/** + * Load a single recipe + * @param id ID of the recipe to load + * @returns A single recipe + */ export async function fetchRecipe(id: string): Promise { const res = await fetch(`${RECIPE_URL}/${id}`) if (!res.ok) { @@ -14,6 +22,23 @@ export async function fetchRecipe(id: string): Promise { return res.json() } +/** + * Load list of all recipes + * @returns Array of recipe + */ +export async function fetchRecipeList(): Promise { + const res = await fetch(`${RECIPE_URL}/`) + if (!res.ok) { + throw new Error(`Failed to fetch recipe list`) + } + return res.json() +} + +/** + * Create new Recipe + * @param recipe Recipe to create + * @returns Saved recipe + */ export async function createRecipe(recipe: Recipe): Promise { const res = await fetch(RECIPE_URL, { method: "POST", @@ -26,6 +51,11 @@ export async function createRecipe(recipe: Recipe): Promise { return res.json() } +/** + * Save an existing recipe + * @param recipe Recipe to save. This recipe must have an ID! + * @returns Saved recipe + */ export async function updateRecipe(recipe: Recipe): Promise { const res = await fetch(`${RECIPE_URL}/${recipe.id}`, { method: "PUT", diff --git a/frontend/src/components/recipes/RecipeDetailPage.tsx b/frontend/src/components/recipes/RecipeDetailPage.tsx index 6fac82b..715b383 100644 --- a/frontend/src/components/recipes/RecipeDetailPage.tsx +++ b/frontend/src/components/recipes/RecipeDetailPage.tsx @@ -1,7 +1,7 @@ import { useParams, Link } from "react-router-dom" -import { recipes } from "../../mock_data/recipes" import type { Recipe } from "../../types/recipe" -import { useState } from "react" +import { useEffect, useState } from "react" +import { fetchRecipe } from "../../api/recipePoint" /** @@ -11,22 +11,43 @@ import { useState } from "react" export default function RecipeDetailPage() { // Extract recipe ID from route params const { id } = useParams<{ id: string }>() - const recipe = recipes.find((r) => r.id === id) + // the recipe loaded from the backend, don't change this! it's required for scaling + const [recipe, setRecipe] = useState(null) + // Working copy for re-calculating ingredients + const [recipeWorkingCopy, setRecipeWorkingCopy] = useState(null) + // load recipe data whenever id changes + useEffect(() => { + const loadRecipe = async () => { + if (id) { + try { + // Fetch recipe data when editing an existing one + console.log("loading recipe with id", id) + const data = await fetchRecipe(id) + setRecipe(data) + } catch (err) { + console.error(err) + } + } + } + + loadRecipe() + }, [id]) + + // set original recipe data and working copy when recipe changes + useEffect( ()=> { + setRecipeWorkingCopy(recipe); + }, [recipe]) - if (!recipe) { + if (!recipe || !recipeWorkingCopy) { return

Recipe not found.

} - - // Working copy for re-calculating ingredients - const [recipeWorkingCopy, updateRecipeWorkingCopy] = useState(recipe) - // Keep original immutable for scaling - const [originalRecipe] = useState(recipe) + /** recalculate ingredients based on the amount of servings */ const recalculateIngredients = (newAmount: number) => { // Always calculate factor from the *original recipe*, not the working copy - const factor = newAmount / originalRecipe.servings.amount + const factor = newAmount / recipe.servings.amount // Create a new ingredient list with updated amounts const updatedIngredientGroupList = recipe.ingredientGroupList.map((ingGrp) => ({ @@ -38,7 +59,7 @@ export default function RecipeDetailPage() { })) // Update working copy with new servings + recalculated ingredients - updateRecipeWorkingCopy({ + setRecipeWorkingCopy({ ...recipeWorkingCopy, servings: { ...recipeWorkingCopy.servings, diff --git a/frontend/src/components/recipes/RecipeEditPage.tsx b/frontend/src/components/recipes/RecipeEditPage.tsx index 73ba44e..624487d 100644 --- a/frontend/src/components/recipes/RecipeEditPage.tsx +++ b/frontend/src/components/recipes/RecipeEditPage.tsx @@ -5,6 +5,7 @@ import RecipeEditor from "./RecipeEditor" import { fetchRecipe, createRecipe, updateRecipe } from "../../api/recipePoint" export default function RecipeEditPage() { + // Extract recipe ID from route params const { id } = useParams<{ id: string }>() const [recipe, setRecipe] = useState(null) const navigate = useNavigate() diff --git a/frontend/src/components/recipes/RecipeListPage.tsx b/frontend/src/components/recipes/RecipeListPage.tsx index 3ea4c35..dc672bc 100644 --- a/frontend/src/components/recipes/RecipeListPage.tsx +++ b/frontend/src/components/recipes/RecipeListPage.tsx @@ -1,17 +1,38 @@ -import { recipes } from "../../mock_data/recipes" +import { useEffect, useState } from "react" import RecipeListItem from "./RecipeListItem" +import type { Recipe } from "../../types/recipe" +import { fetchRecipeList } from "../../api/recipePoint" /** * Displays a list of recipes in a sidebar layout. * Each recipe link fills the available width. */ export default function RecipeListPage() { + + const [recipeList, setRecipeList] = useState(null) + // load recipes once on render + useEffect(() => { + const loadRecipeList = async () => { + try { + // Fetch recipe data when editing an existing one + console.log("loading recipe list") + const data = await fetchRecipeList() + setRecipeList(data) + } catch (err) { + console.error(err) + } + } + loadRecipeList() + }, []) + + if(!recipeList) { return
Unable to load recipes1
} + // @todo find a better representation than an oldfashioned sidebar return (

Recipes

- {recipes.map((recipe) => ( + {recipeList.map((recipe) => (