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; let recipeList = recipes;
// Routes // 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) => { 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); 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"); recipe ? res.json(recipe) : res.status(404).send("Recipe not found");
}); });
app.post("/recipe", (req, res) => { app.post("/recipe", (req, res) => {
console.log("POST /recipe")
const newRecipe: Recipe = { id: uuidv4(), ...req.body }; const newRecipe: Recipe = { id: uuidv4(), ...req.body };
recipeList.push(newRecipe); recipeList.push(newRecipe);
res.status(201).json(newRecipe); res.status(201).json(newRecipe);
}); });
app.put("/recipe/:id", (req, res) => { app.put("/recipe/:id", (req, res) => {
const index = recipes.findIndex(r => r.id === req.params.id); let recipeId : string = req.params.id;
if (index === -1) return res.status(404).send("Recipe not found"); 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 }; recipeList[index] = { ...recipeList[index], ...req.body };
res.json(recipeList[index]); res.json(recipeList[index]);
}); });

View file

@ -4,8 +4,16 @@ import { API_BASE_URL } from "../config/api"
/** /**
* Util for handling the recipe api * Util for handling the recipe api
*/ */
/**
* URL for handling recipes
*/
const RECIPE_URL = `${API_BASE_URL}/recipe` 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> { export async function fetchRecipe(id: string): Promise<Recipe> {
const res = await fetch(`${RECIPE_URL}/${id}`) const res = await fetch(`${RECIPE_URL}/${id}`)
if (!res.ok) { if (!res.ok) {
@ -14,6 +22,23 @@ export async function fetchRecipe(id: string): Promise<Recipe> {
return res.json() 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> { export async function createRecipe(recipe: Recipe): Promise<Recipe> {
const res = await fetch(RECIPE_URL, { const res = await fetch(RECIPE_URL, {
method: "POST", method: "POST",
@ -26,6 +51,11 @@ export async function createRecipe(recipe: Recipe): Promise<Recipe> {
return res.json() 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> { export async function updateRecipe(recipe: Recipe): Promise<Recipe> {
const res = await fetch(`${RECIPE_URL}/${recipe.id}`, { const res = await fetch(`${RECIPE_URL}/${recipe.id}`, {
method: "PUT", method: "PUT",

View file

@ -1,7 +1,7 @@
import { useParams, Link } from "react-router-dom" import { useParams, Link } from "react-router-dom"
import { recipes } from "../../mock_data/recipes"
import type { Recipe } from "../../types/recipe" 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() { export default function RecipeDetailPage() {
// Extract recipe ID from route params // Extract recipe ID from route params
const { id } = useParams<{ id: string }>() 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> 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 */ /** recalculate ingredients based on the amount of servings */
const recalculateIngredients = (newAmount: number) => { const recalculateIngredients = (newAmount: number) => {
// Always calculate factor from the *original recipe*, not the working copy // 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 // Create a new ingredient list with updated amounts
const updatedIngredientGroupList = recipe.ingredientGroupList.map((ingGrp) => ({ const updatedIngredientGroupList = recipe.ingredientGroupList.map((ingGrp) => ({
@ -38,7 +59,7 @@ export default function RecipeDetailPage() {
})) }))
// Update working copy with new servings + recalculated ingredients // Update working copy with new servings + recalculated ingredients
updateRecipeWorkingCopy({ setRecipeWorkingCopy({
...recipeWorkingCopy, ...recipeWorkingCopy,
servings: { servings: {
...recipeWorkingCopy.servings, ...recipeWorkingCopy.servings,

View file

@ -5,6 +5,7 @@ import RecipeEditor from "./RecipeEditor"
import { fetchRecipe, createRecipe, updateRecipe } from "../../api/recipePoint" import { fetchRecipe, createRecipe, updateRecipe } from "../../api/recipePoint"
export default function RecipeEditPage() { export default function RecipeEditPage() {
// Extract recipe ID from route params
const { id } = useParams<{ id: string }>() const { id } = useParams<{ id: string }>()
const [recipe, setRecipe] = useState<Recipe | null>(null) const [recipe, setRecipe] = useState<Recipe | null>(null)
const navigate = useNavigate() 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 RecipeListItem from "./RecipeListItem"
import type { Recipe } from "../../types/recipe"
import { fetchRecipeList } from "../../api/recipePoint"
/** /**
* Displays a list of recipes in a sidebar layout. * Displays a list of recipes in a sidebar layout.
* Each recipe link fills the available width. * Each recipe link fills the available width.
*/ */
export default function RecipeListPage() { 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 ( return (
<div className="sidebar"> <div className="sidebar">
<h1 className="sidebar-title">Recipes</h1> <h1 className="sidebar-title">Recipes</h1>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
{recipes.map((recipe) => ( {recipeList.map((recipe) => (
<RecipeListItem <RecipeListItem
key={recipe.id} key={recipe.id}
title = {recipe.title} title = {recipe.title}