Load and save recipes from backend
This commit is contained in:
parent
38a5707622
commit
568606213d
5 changed files with 102 additions and 16 deletions
|
|
@ -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]);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue