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,7 +1,8 @@
|
|||
import { BrowserRouter as Router, Routes, Route } from "react-router-dom"
|
||||
import RecipeListView from "./components/RecipeListView"
|
||||
import RecipeDetailView from "./components/RecipeDetailView"
|
||||
import RecipeEditView from "./components/RecipeEditView"
|
||||
import RecipeDetailPage from "./components/recipes/RecipeDetailPage"
|
||||
import RecipeEditPage from "./components/recipes/RecipeEditPage"
|
||||
import RecipeListPage from "./components/recipes/RecipeListPage"
|
||||
|
||||
import "./App.css"
|
||||
|
||||
/**
|
||||
|
|
@ -13,13 +14,13 @@ function App() {
|
|||
<Router>
|
||||
<Routes>
|
||||
{/* Home page: list of recipes */}
|
||||
<Route path="/" element={<RecipeListView />} />
|
||||
<Route path="/" element={<RecipeListPage />} />
|
||||
|
||||
{/* Detail page: shows one recipe */}
|
||||
<Route path="/recipe/:id" element={<RecipeDetailView />} />
|
||||
<Route path="/recipe/:id" element={<RecipeDetailPage />} />
|
||||
|
||||
{/* Edit page: form to edit a recipe */}
|
||||
<Route path="/recipe/:id/edit" element={<RecipeEditView />} />
|
||||
<Route path="/recipe/:id/edit" element={<RecipeEditPage />} />
|
||||
</Routes>
|
||||
</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
|
||||
*/
|
||||
|
||||
import type { Ingredient } from "../types/ingredient"
|
||||
import type { IngredientGroup } from "../types/ingredientGroup"
|
||||
import type { Ingredient } from "../../types/ingredient"
|
||||
import type { IngredientGroup } from "../../types/ingredientGroup"
|
||||
import { IngredientListEditor } from "./IngredientListEditor"
|
||||
|
||||
type IngredientGroupListEditorProps = {
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import type { Ingredient } from "../types/ingredient"
|
||||
import type { Ingredient } from "../../types/ingredient"
|
||||
|
||||
/**
|
||||
* Editor for handling the ingredient list
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { useParams, Link } from "react-router-dom"
|
||||
import { recipes } from "../mock_data/recipes"
|
||||
import type { Recipe } from "../types/recipe"
|
||||
import { recipes } from "../../mock_data/recipes"
|
||||
import type { Recipe } from "../../types/recipe"
|
||||
import { useState } from "react"
|
||||
|
||||
|
||||
|
|
@ -8,7 +8,7 @@ import { useState } from "react"
|
|||
* Displays the full detail of a single recipe,
|
||||
* including its ingredients, instructions, and image.
|
||||
*/
|
||||
export default function RecipeDetailView() {
|
||||
export default function RecipeDetailPage() {
|
||||
// Extract recipe ID from route params
|
||||
const { id } = useParams<{ id: string }>()
|
||||
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 type { Recipe } from "../types/recipe"
|
||||
import type { IngredientGroup } from "../types/ingredientGroup"
|
||||
import type { Recipe } from "../../types/recipe"
|
||||
import type { IngredientGroup } from "../../types/ingredientGroup"
|
||||
import { IngredientGroupListEditor } from "./IngredientGroupListEditor"
|
||||
import { IngredientListEditor } from "./IngredientListEditor"
|
||||
|
||||
type RecipeEditorProps = {
|
||||
recipe: Recipe
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
import { recipes } from "../mock_data/recipes"
|
||||
import { recipes } from "../../mock_data/recipes"
|
||||
import RecipeListItem from "./RecipeListItem"
|
||||
|
||||
/**
|
||||
* Displays a list of recipes in a sidebar layout.
|
||||
* Each recipe link fills the available width.
|
||||
*/
|
||||
export default function RecipeListView() {
|
||||
export default function RecipeListPage() {
|
||||
return (
|
||||
<div className="sidebar">
|
||||
<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