Load and save recipes from backend

This commit is contained in:
Anika Raemer 2025-09-12 19:25:50 +02:00
parent 38a5707622
commit 568606213d
5 changed files with 102 additions and 16 deletions

View file

@ -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]);
});

View file

@ -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<Recipe> {
const res = await fetch(`${RECIPE_URL}/${id}`)
if (!res.ok) {
@ -14,6 +22,23 @@ export async function fetchRecipe(id: string): Promise<Recipe> {
return res.json()
}
/**
* Load list of all recipes
* @returns Array of recipe
*/
export async function fetchRecipeList(): Promise<Recipe[]> {
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<Recipe> {
const res = await fetch(RECIPE_URL, {
method: "POST",
@ -26,6 +51,11 @@ export async function createRecipe(recipe: Recipe): Promise<Recipe> {
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<Recipe> {
const res = await fetch(`${RECIPE_URL}/${recipe.id}`, {
method: "PUT",

View file

@ -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<Recipe | null>(null)
// Working copy for re-calculating ingredients
const [recipeWorkingCopy, setRecipeWorkingCopy] = useState<Recipe|null>(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 <p className="p-6">Recipe not found.</p>
}
// Working copy for re-calculating ingredients
const [recipeWorkingCopy, updateRecipeWorkingCopy] = useState<Recipe>(recipe)
// Keep original immutable for scaling
const [originalRecipe] = useState<Recipe>(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,

View file

@ -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<Recipe | null>(null)
const navigate = useNavigate()

View file

@ -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<Recipe[]|null>(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 <div>Unable to load recipes1</div>}
// @todo find a better representation than an oldfashioned sidebar
return (
<div className="sidebar">
<h1 className="sidebar-title">Recipes</h1>
<div className="flex flex-col gap-2">
{recipes.map((recipe) => (
{recipeList.map((recipe) => (
<RecipeListItem
key={recipe.id}
title = {recipe.title}