renaming, restructuring, adding an api util to the frontend (currently editPage only and a mock backend
This commit is contained in:
parent
1bd1952ecb
commit
38a5707622
16 changed files with 247 additions and 117 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
import { Recipe } from "../types/recipe"
|
import type { Recipe } from "../types/recipe"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mock data set with some sample recipes.
|
* Mock data set with some sample recipes.
|
||||||
|
|
@ -8,12 +8,56 @@ export const recipes: Recipe[] = [
|
||||||
{
|
{
|
||||||
id: "1",
|
id: "1",
|
||||||
title: "Spaghetti Bolognese",
|
title: "Spaghetti Bolognese",
|
||||||
ingredients: [
|
servings: { amount: 1, unit: "Person"},
|
||||||
|
ingredientGroupList: [
|
||||||
|
{
|
||||||
|
ingredientList: [
|
||||||
{ name: "Spaghetti", amount: 200, unit: "g" },
|
{ name: "Spaghetti", amount: 200, unit: "g" },
|
||||||
{ name: "Ground Beef", amount: 300, unit: "g" },
|
{ name: "Ground Beef", amount: 300, unit: "g" },
|
||||||
{ name: "Tomato Sauce", amount: 400, unit: "ml" }
|
{ name: "Tomato Sauce", amount: 400, unit: "ml" }
|
||||||
|
]
|
||||||
|
}
|
||||||
],
|
],
|
||||||
instructions: "Cook pasta. Prepare sauce. Mix together. Serve hot.",
|
instructions: "Cook pasta. Prepare sauce. Mix together. Serve hot.",
|
||||||
photoUrl: "https://source.unsplash.com/400x300/?spaghetti"
|
//imageUrl: "https://source.unsplash.com/400x300/?spaghetti"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "2",
|
||||||
|
title: "Spaghetti Carbonara",
|
||||||
|
servings: { amount: 4, unit: "Persons"},
|
||||||
|
ingredientGroupList: [
|
||||||
|
{
|
||||||
|
ingredientList:[
|
||||||
|
{ name: "Spaghetti", amount: 500, unit: "g" },
|
||||||
|
{ name: "Bacon", amount: 150, unit: "g" },
|
||||||
|
{ name: "Cream", amount: 200, unit: "ml" },
|
||||||
|
{ name: "Onion", amount: 1},
|
||||||
|
{ name: "Parmesan cheese", amount: 200, unit: "g"},
|
||||||
|
{ name: "Olives", amount: 100, unit: "g"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
instructions: "Cook pasta. Prepare sauce. Mix together. Serve hot.",
|
||||||
|
//imageUrl: "https://source.unsplash.com/400x300/?spaghetti"
|
||||||
|
},
|
||||||
|
{ id: "3",
|
||||||
|
title: "Apfelkuchen Edeltrud",
|
||||||
|
servings: { amount: 1, unit: "Kuchen"},
|
||||||
|
ingredientGroupList:[
|
||||||
|
{
|
||||||
|
title: "Fuer den Teig",
|
||||||
|
ingredientList: [
|
||||||
|
{ name: "Mehl", amount: 400, unit: "g" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Fuer die Fuellung",
|
||||||
|
ingredientList:[
|
||||||
|
{name: "Aepfel", amount: 4},
|
||||||
|
{name: "Rosinen", amount: 1, unit: "Hand voll"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
instructions: "Einen Muerbteig von 400 g Mehl zubereiten"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
@ -2,37 +2,38 @@ import express from "express";
|
||||||
import cors from "cors";
|
import cors from "cors";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
import { recipes } from "../mock_data/recipes"
|
import { recipes } from "./mock_data/recipes"
|
||||||
import { Recipe } from "../types/recipe"
|
import { Recipe } from "./types/recipe"
|
||||||
|
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
app.use(cors());
|
app.use(cors());
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
|
||||||
|
let recipeList = recipes;
|
||||||
// Routes
|
// Routes
|
||||||
app.get("/recipes", (req, res) => res.json(recipes));
|
app.get("/recipe", (req, res) => res.json(recipeList));
|
||||||
|
|
||||||
app.get("/recipes/:id", (req, res) => {
|
app.get("/recipe/:id", (req, res) => {
|
||||||
const recipe = recipes.find(r => r.id === req.params.id);
|
const recipe = recipeList.find(r => r.id === req.params.id);
|
||||||
recipe ? res.json(recipe) : res.status(404).send("Recipe not found");
|
recipe ? res.json(recipe) : res.status(404).send("Recipe not found");
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post("/recipes", (req, res) => {
|
app.post("/recipe", (req, res) => {
|
||||||
const newRecipe: Recipe = { id: uuidv4(), ...req.body };
|
const newRecipe: Recipe = { id: uuidv4(), ...req.body };
|
||||||
recipes.push(newRecipe);
|
recipeList.push(newRecipe);
|
||||||
res.status(201).json(newRecipe);
|
res.status(201).json(newRecipe);
|
||||||
});
|
});
|
||||||
|
|
||||||
app.put("/recipes/:id", (req, res) => {
|
app.put("/recipe/:id", (req, res) => {
|
||||||
const index = recipes.findIndex(r => r.id === req.params.id);
|
const index = recipes.findIndex(r => r.id === req.params.id);
|
||||||
if (index === -1) return res.status(404).send("Recipe not found");
|
if (index === -1) return res.status(404).send("Recipe not found");
|
||||||
recipes[index] = { ...recipes[index], ...req.body };
|
recipeList[index] = { ...recipeList[index], ...req.body };
|
||||||
res.json(recipes[index]);
|
res.json(recipeList[index]);
|
||||||
});
|
});
|
||||||
|
|
||||||
app.delete("/recipes/:id", (req, res) => {
|
app.delete("/recipe/:id", (req, res) => {
|
||||||
recipes = recipes.filter(r => r.id !== req.params.id);
|
recipeList = recipeList.filter(r => r.id !== req.params.id);
|
||||||
res.status(204).send();
|
res.status(204).send();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
18
backend/src/types/ingredientGroup.ts
Normal file
18
backend/src/types/ingredientGroup.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
import type { Ingredient } from "./ingredient"
|
||||||
|
/**
|
||||||
|
* A group of ingredients
|
||||||
|
* Consisting of title and ingredient list, this interface is used to group
|
||||||
|
* the ingredients for a specific part of the dish, e.g., dough, filling and
|
||||||
|
* icing of a cake
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface IngredientGroup {
|
||||||
|
/**
|
||||||
|
* Title of the group describing its purpose
|
||||||
|
* The title is optional as recipes consisting of a single ingredient group usually don't
|
||||||
|
* supply a title
|
||||||
|
*/
|
||||||
|
title? : string
|
||||||
|
/** Ingredients */
|
||||||
|
ingredientList : Ingredient[]
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,16 @@
|
||||||
import { Ingredient } from "./ingredient"
|
import type { IngredientGroup } from "./ingredientGroup"
|
||||||
|
import type { Servings } from "./servings"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a recipe object in the application.
|
* Represents a recipe object in the application.
|
||||||
*/
|
*/
|
||||||
|
/*
|
||||||
|
* @todo ingredient groups! There may be serveral ingredient lists, each with a title.
|
||||||
|
* e.g. for the dough, for the filling, for the icing,...
|
||||||
|
* - add type ingredient group with an optional title and a list of ingredients
|
||||||
|
* - adapt RecipeDetailView
|
||||||
|
* - add an IngredientGroupListEditor for handling IngredientGroups
|
||||||
|
*/
|
||||||
export interface Recipe {
|
export interface Recipe {
|
||||||
/** Unique identifier for the recipe */
|
/** Unique identifier for the recipe */
|
||||||
id: string
|
id: string
|
||||||
|
|
@ -10,13 +18,17 @@ export interface Recipe {
|
||||||
/** Title of the recipe */
|
/** Title of the recipe */
|
||||||
title: string
|
title: string
|
||||||
|
|
||||||
/** List of ingredients with amount + unit */
|
/** List of ingredients groups containing the ingredients of the recipe */
|
||||||
ingredients: Ingredient[]
|
ingredientGroupList: IngredientGroup[]
|
||||||
|
|
||||||
/** Preparation instructions */
|
/** Preparation instructions */
|
||||||
instructions: string
|
instructions: string
|
||||||
|
|
||||||
|
/** Number of servings, e.g., for 4 person, 12 cupcakes, 2 glasses */
|
||||||
|
servings: Servings
|
||||||
|
|
||||||
|
/** Unit for the quantity */
|
||||||
|
|
||||||
/** Optional image URL for the recipe */
|
/** Optional image URL for the recipe */
|
||||||
imageUrl?: string
|
imageUrl?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
12
backend/src/types/servings.ts
Normal file
12
backend/src/types/servings.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
/**
|
||||||
|
* Defines how many servings of the dish are prepared when following the recipe
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface Servings{
|
||||||
|
/** Amount of servings */
|
||||||
|
amount: number,
|
||||||
|
|
||||||
|
/** Unit, e.g. 4 persons, 2 glasses, 12 cupcakes */
|
||||||
|
unit: string
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import { BrowserRouter as Router, Routes, Route } from "react-router-dom"
|
import { BrowserRouter as Router, Routes, Route } from "react-router-dom"
|
||||||
import RecipeListView from "./components/RecipeListView"
|
import RecipeDetailPage from "./components/recipes/RecipeDetailPage"
|
||||||
import RecipeDetailView from "./components/RecipeDetailView"
|
import RecipeEditPage from "./components/recipes/RecipeEditPage"
|
||||||
import RecipeEditView from "./components/RecipeEditView"
|
import RecipeListPage from "./components/recipes/RecipeListPage"
|
||||||
|
|
||||||
import "./App.css"
|
import "./App.css"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -13,13 +14,13 @@ function App() {
|
||||||
<Router>
|
<Router>
|
||||||
<Routes>
|
<Routes>
|
||||||
{/* Home page: list of recipes */}
|
{/* Home page: list of recipes */}
|
||||||
<Route path="/" element={<RecipeListView />} />
|
<Route path="/" element={<RecipeListPage />} />
|
||||||
|
|
||||||
{/* Detail page: shows one recipe */}
|
{/* Detail page: shows one recipe */}
|
||||||
<Route path="/recipe/:id" element={<RecipeDetailView />} />
|
<Route path="/recipe/:id" element={<RecipeDetailPage />} />
|
||||||
|
|
||||||
{/* Edit page: form to edit a recipe */}
|
{/* Edit page: form to edit a recipe */}
|
||||||
<Route path="/recipe/:id/edit" element={<RecipeEditView />} />
|
<Route path="/recipe/:id/edit" element={<RecipeEditPage />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</Router>
|
</Router>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
39
frontend/src/api/recipePoint.ts
Normal file
39
frontend/src/api/recipePoint.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
import type { Recipe } from "../types/recipe"
|
||||||
|
import { API_BASE_URL } from "../config/api"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Util for handling the recipe api
|
||||||
|
*/
|
||||||
|
const RECIPE_URL = `${API_BASE_URL}/recipe`
|
||||||
|
|
||||||
|
export async function fetchRecipe(id: string): Promise<Recipe> {
|
||||||
|
const res = await fetch(`${RECIPE_URL}/${id}`)
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`Failed to fetch recipe with id ${id}`)
|
||||||
|
}
|
||||||
|
return res.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createRecipe(recipe: Recipe): Promise<Recipe> {
|
||||||
|
const res = await fetch(RECIPE_URL, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(recipe),
|
||||||
|
})
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error("Failed to create recipe")
|
||||||
|
}
|
||||||
|
return res.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateRecipe(recipe: Recipe): Promise<Recipe> {
|
||||||
|
const res = await fetch(`${RECIPE_URL}/${recipe.id}`, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(recipe),
|
||||||
|
})
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`Failed to update recipe with id ${recipe.id}`)
|
||||||
|
}
|
||||||
|
return res.json()
|
||||||
|
}
|
||||||
|
|
@ -1,76 +0,0 @@
|
||||||
import { useParams, useNavigate } from "react-router-dom"
|
|
||||||
//import { useEffect, useState } from "react"
|
|
||||||
import type { Recipe } from "../types/recipe"
|
|
||||||
import RecipeEditor from "./RecipeEditor"
|
|
||||||
import { recipes } from "../mock_data/recipes"
|
|
||||||
|
|
||||||
|
|
||||||
export default function RecipeEditView() {
|
|
||||||
const { id } = useParams<{ id: string }>()
|
|
||||||
const navigate = useNavigate()
|
|
||||||
//const [recipe, setRecipe] = useState<Recipe | null>(null)
|
|
||||||
console.log("searching for recipe with id", id)
|
|
||||||
const recipe = recipes.find((r) => r.id === id)
|
|
||||||
|
|
||||||
// Fetch recipe data when editing an existing one
|
|
||||||
/*seEffect(() => {
|
|
||||||
if (id) {
|
|
||||||
fetch(`http://localhost:4000/recipes/${id}`)
|
|
||||||
.then(res => res.json())
|
|
||||||
.then(data => setRecipe(data))
|
|
||||||
} else {
|
|
||||||
// new recipe case
|
|
||||||
setRecipe({
|
|
||||||
id: "",
|
|
||||||
title: "",
|
|
||||||
ingredients: [],
|
|
||||||
instructions: "",
|
|
||||||
photoUrl: "",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, [id])
|
|
||||||
|
|
||||||
const handleSave = async (updated: Recipe) => {
|
|
||||||
if (updated.id) {
|
|
||||||
await fetch(`http://localhost:4000/recipes/${updated.id}`, {
|
|
||||||
method: "PUT",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify(updated),
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
await fetch("http://localhost:4000/recipes", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify(updated),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
navigate("/") // back to list
|
|
||||||
}
|
|
||||||
if (!recipe) return <div>Loading...</div>
|
|
||||||
*/
|
|
||||||
const handleSave = (updatedRecipe : Recipe) => {
|
|
||||||
console.log("Saving", updatedRecipe.title)
|
|
||||||
navigateBack();
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleCancel = () => {
|
|
||||||
console.log("Cancelling edit mode for", recipe ? recipe.id : "no recipe at all")
|
|
||||||
navigateBack();
|
|
||||||
}
|
|
||||||
|
|
||||||
const navigateBack = () => {
|
|
||||||
if(recipe && recipe.id){
|
|
||||||
console.log("navigating to recipe with id", recipe.id)
|
|
||||||
navigate("/recipe/" + recipe.id) // go back to detail view
|
|
||||||
} else {
|
|
||||||
console.log("navigating back to list as no recipe was selected")
|
|
||||||
navigate("/") // no recipe -> go back to list
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// error handling -> if there is no recipe, we cannot open edit view
|
|
||||||
if (!recipe) return <div>Oops, there's no recipe in RecipeEditView</div>
|
|
||||||
console.log("opening recipe in edit mode", recipe.title, id)
|
|
||||||
|
|
||||||
|
|
||||||
return <RecipeEditor recipe={recipe} onSave={handleSave} onCancel={handleCancel}/>
|
|
||||||
}
|
|
||||||
|
|
@ -2,8 +2,8 @@
|
||||||
* Editor for ingredient groups
|
* Editor for ingredient groups
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Ingredient } from "../types/ingredient"
|
import type { Ingredient } from "../../types/ingredient"
|
||||||
import type { IngredientGroup } from "../types/ingredientGroup"
|
import type { IngredientGroup } from "../../types/ingredientGroup"
|
||||||
import { IngredientListEditor } from "./IngredientListEditor"
|
import { IngredientListEditor } from "./IngredientListEditor"
|
||||||
|
|
||||||
type IngredientGroupListEditorProps = {
|
type IngredientGroupListEditorProps = {
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import type { Ingredient } from "../types/ingredient"
|
import type { Ingredient } from "../../types/ingredient"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Editor for handling the ingredient list
|
* Editor for handling the ingredient list
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { useParams, Link } from "react-router-dom"
|
import { useParams, Link } from "react-router-dom"
|
||||||
import { recipes } from "../mock_data/recipes"
|
import { recipes } from "../../mock_data/recipes"
|
||||||
import type { Recipe } from "../types/recipe"
|
import type { Recipe } from "../../types/recipe"
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -8,7 +8,7 @@ import { useState } from "react"
|
||||||
* Displays the full detail of a single recipe,
|
* Displays the full detail of a single recipe,
|
||||||
* including its ingredients, instructions, and image.
|
* including its ingredients, instructions, and image.
|
||||||
*/
|
*/
|
||||||
export default function RecipeDetailView() {
|
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)
|
const recipe = recipes.find((r) => r.id === id)
|
||||||
76
frontend/src/components/recipes/RecipeEditPage.tsx
Normal file
76
frontend/src/components/recipes/RecipeEditPage.tsx
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
import { useParams, useNavigate } from "react-router-dom"
|
||||||
|
import { useEffect, useState } from "react"
|
||||||
|
import type { Recipe } from "../../types/recipe"
|
||||||
|
import RecipeEditor from "./RecipeEditor"
|
||||||
|
import { fetchRecipe, createRecipe, updateRecipe } from "../../api/recipePoint"
|
||||||
|
|
||||||
|
export default function RecipeEditPage() {
|
||||||
|
const { id } = useParams<{ id: string }>()
|
||||||
|
const [recipe, setRecipe] = useState<Recipe | null>(null)
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const loadRecipe = async () => {
|
||||||
|
if (id) {
|
||||||
|
try {
|
||||||
|
// Fetch recipe data when editing an existing one
|
||||||
|
const data = await fetchRecipe(id)
|
||||||
|
setRecipe(data)
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// New recipe case
|
||||||
|
setRecipe({
|
||||||
|
id: "",
|
||||||
|
title: "",
|
||||||
|
ingredientGroupList: [
|
||||||
|
],
|
||||||
|
instructions: "",
|
||||||
|
servings: {
|
||||||
|
amount: 1,
|
||||||
|
unit: ""
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadRecipe()
|
||||||
|
}, [id])
|
||||||
|
|
||||||
|
const handleSave = async (updated: Recipe) => {
|
||||||
|
try {
|
||||||
|
if (updated.id) {
|
||||||
|
await updateRecipe(updated)
|
||||||
|
} else {
|
||||||
|
await createRecipe(updated)
|
||||||
|
}
|
||||||
|
navigateBack();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
console.log("Cancelling edit mode for", recipe ? recipe.id : "no recipe at all")
|
||||||
|
navigateBack();
|
||||||
|
}
|
||||||
|
|
||||||
|
const navigateBack = () => {
|
||||||
|
if(recipe && recipe.id){
|
||||||
|
console.log("navigating to recipe with id", recipe.id)
|
||||||
|
navigate("/recipe/" + recipe.id) // go back to detail view
|
||||||
|
} else {
|
||||||
|
console.log("navigating back to list as no recipe was selected")
|
||||||
|
navigate("/") // no recipe -> go back to list
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// error handling -> if there is no recipe, we cannot open edit view
|
||||||
|
if (!recipe) {
|
||||||
|
return <div>Loading...</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
return <RecipeEditor recipe={recipe} onSave={handleSave} onCancel={handleCancel}/>
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
import type { Recipe } from "../types/recipe"
|
import type { Recipe } from "../../types/recipe"
|
||||||
import type { IngredientGroup } from "../types/ingredientGroup"
|
import type { IngredientGroup } from "../../types/ingredientGroup"
|
||||||
import { IngredientGroupListEditor } from "./IngredientGroupListEditor"
|
import { IngredientGroupListEditor } from "./IngredientGroupListEditor"
|
||||||
import { IngredientListEditor } from "./IngredientListEditor"
|
|
||||||
|
|
||||||
type RecipeEditorProps = {
|
type RecipeEditorProps = {
|
||||||
recipe: Recipe
|
recipe: Recipe
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import { recipes } from "../mock_data/recipes"
|
import { recipes } from "../../mock_data/recipes"
|
||||||
import RecipeListItem from "./RecipeListItem"
|
import RecipeListItem from "./RecipeListItem"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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 RecipeListView() {
|
export default function RecipeListPage() {
|
||||||
return (
|
return (
|
||||||
<div className="sidebar">
|
<div className="sidebar">
|
||||||
<h1 className="sidebar-title">Recipes</h1>
|
<h1 className="sidebar-title">Recipes</h1>
|
||||||
4
frontend/src/config/api.ts
Normal file
4
frontend/src/config/api.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
/**
|
||||||
|
* Backend URL
|
||||||
|
*/
|
||||||
|
export const API_BASE_URL = "http://localhost:4000"
|
||||||
Loading…
Add table
Add a link
Reference in a new issue