edit recipe
This commit is contained in:
parent
798e879863
commit
8b52d31dcf
10 changed files with 389 additions and 217 deletions
|
|
@ -50,6 +50,7 @@ export const addRecipe = async (req: Request, res: Response): Promise<void> => {
|
|||
return;
|
||||
}
|
||||
try {
|
||||
console.log(req.body);
|
||||
const createdRecipe = await model.addRecipe(req.body);
|
||||
res.status(201).json(createdRecipe);
|
||||
} catch (error) {
|
||||
|
|
@ -61,6 +62,27 @@ export const addRecipe = async (req: Request, res: Response): Promise<void> => {
|
|||
}
|
||||
};
|
||||
|
||||
export const updateRecipe = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
): Promise<void> => {
|
||||
console.log(req.body);
|
||||
const id = parseInt(req.params.id, 10);
|
||||
if (process.env.NODE_ENV === "demo") {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const updatedRecipe = await model.updateRecipe(req.body, id);
|
||||
res.status(201).json(updatedRecipe);
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
msg: "Failed to add recipe",
|
||||
source: "recipeController",
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const setStars = async (req: Request, res: Response): Promise<void> => {
|
||||
if (process.env.NODE_ENV === "demo") {
|
||||
return;
|
||||
|
|
@ -104,6 +126,7 @@ export default {
|
|||
getAllRecipes,
|
||||
getRecipeById,
|
||||
addRecipe,
|
||||
updateRecipe,
|
||||
setStars,
|
||||
deleteRecipe,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ class RecipeModel {
|
|||
|
||||
async getAllRecipes(): Promise<any[]> {
|
||||
try {
|
||||
logger.info("index page view")
|
||||
logger.info("index page view");
|
||||
return await this.prisma.recipes.findMany();
|
||||
} catch (err) {
|
||||
console.error("Error fetching all recipes:", err);
|
||||
|
|
@ -46,7 +46,10 @@ class RecipeModel {
|
|||
instruction: step.instruction,
|
||||
})),
|
||||
};
|
||||
logger.info("recipe page view", { recipe_id: data.details.id, recipe_name: data.details.name })
|
||||
logger.info("recipe page view", {
|
||||
recipe_id: data.details.id,
|
||||
recipe_name: data.details.name,
|
||||
});
|
||||
return data;
|
||||
} catch (err) {
|
||||
console.log("Error finding recipe:", err);
|
||||
|
|
@ -58,25 +61,25 @@ class RecipeModel {
|
|||
}
|
||||
|
||||
async addRecipe(recipeData: {
|
||||
details: {
|
||||
name: string;
|
||||
author: string;
|
||||
cuisine: string;
|
||||
stars: number;
|
||||
ingredients: string[];
|
||||
steps: { [key: string]: string };
|
||||
prep_minutes: number;
|
||||
cook_minutes: number;
|
||||
};
|
||||
ingredients: string[];
|
||||
steps: { [key: string]: string };
|
||||
}): Promise<any> {
|
||||
const {
|
||||
name,
|
||||
author,
|
||||
cuisine,
|
||||
stars,
|
||||
ingredients,
|
||||
steps,
|
||||
prep_minutes,
|
||||
cook_minutes,
|
||||
} = recipeData;
|
||||
const name = recipeData.details.name;
|
||||
const author = recipeData.details.author;
|
||||
const cuisine = recipeData.details.cuisine;
|
||||
const stars = recipeData.details.stars;
|
||||
const prep_minutes = recipeData.details.prep_minutes;
|
||||
const cook_minutes = recipeData.details.cook_minutes;
|
||||
const ingredients = recipeData.ingredients;
|
||||
const steps = recipeData.steps;
|
||||
try {
|
||||
const createdRecipe = await this.prisma.recipes.create({
|
||||
data: {
|
||||
|
|
@ -90,10 +93,7 @@ class RecipeModel {
|
|||
create: ingredients.map((ing) => ({ raw: ing })),
|
||||
},
|
||||
recipeSteps: {
|
||||
create: Object.keys(steps).map((stepNumber) => ({
|
||||
step_number: parseInt(stepNumber),
|
||||
instruction: steps[stepNumber],
|
||||
})),
|
||||
create: steps,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
@ -112,6 +112,67 @@ class RecipeModel {
|
|||
}
|
||||
}
|
||||
|
||||
async updateRecipe(
|
||||
recipeData: {
|
||||
details: {
|
||||
name: string;
|
||||
author: string;
|
||||
cuisine: string;
|
||||
stars: number;
|
||||
prep_minutes: number;
|
||||
cook_minutes: number;
|
||||
};
|
||||
ingredients: string[];
|
||||
steps: { [key: string]: string };
|
||||
},
|
||||
id: number,
|
||||
): Promise<any> {
|
||||
const name = recipeData.details.name;
|
||||
const author = recipeData.details.author;
|
||||
const cuisine = recipeData.details.cuisine;
|
||||
const stars = recipeData.details.stars;
|
||||
const prep_minutes = recipeData.details.prep_minutes;
|
||||
const cook_minutes = recipeData.details.cook_minutes;
|
||||
const ingredients = recipeData.ingredients;
|
||||
const steps = recipeData.steps;
|
||||
try {
|
||||
await this.deleteRecipeData(id);
|
||||
const updatedRecipe = await this.prisma.recipes.update({
|
||||
where: { id },
|
||||
data: {
|
||||
name,
|
||||
author,
|
||||
cuisine,
|
||||
prep_minutes,
|
||||
cook_minutes,
|
||||
stars,
|
||||
recipeIngredients: {
|
||||
create: ingredients.map((ing) => ({ raw: ing })),
|
||||
},
|
||||
recipeSteps: {
|
||||
create: steps,
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!updatedRecipe) {
|
||||
logger.warn(`Recipe with id ${id} cannot be found`);
|
||||
return null;
|
||||
}
|
||||
|
||||
logger.info("Updated Recipe", {
|
||||
id: updatedRecipe.id,
|
||||
name: updatedRecipe.name,
|
||||
});
|
||||
return updatedRecipe;
|
||||
} catch (err) {
|
||||
console.log("Error updating recipe:", err);
|
||||
logger.error("Error updating recipe", {
|
||||
message: err instanceof Error ? err.message : "Unknown error",
|
||||
});
|
||||
throw new Error("Failed to update recipe");
|
||||
}
|
||||
}
|
||||
|
||||
async setStars(id: number, stars: number): Promise<{ message: string }> {
|
||||
try {
|
||||
await this.prisma.recipes.update({
|
||||
|
|
@ -130,13 +191,7 @@ class RecipeModel {
|
|||
|
||||
async deleteRecipe(id: number): Promise<{ message: string }> {
|
||||
try {
|
||||
await this.prisma.recipe_ingredients.deleteMany({
|
||||
where: { recipe_id: id },
|
||||
});
|
||||
|
||||
await this.prisma.recipe_steps.deleteMany({
|
||||
where: { recipe_id: id },
|
||||
});
|
||||
this.deleteRecipeData(id);
|
||||
const deletedRecipe = await this.prisma.recipes.delete({
|
||||
where: { id },
|
||||
});
|
||||
|
|
@ -153,6 +208,25 @@ class RecipeModel {
|
|||
throw new Error(err instanceof Error ? err.message : "Unknown error");
|
||||
}
|
||||
}
|
||||
|
||||
async deleteRecipeData(id: number): Promise<{ message: string }> {
|
||||
try {
|
||||
await this.prisma.recipe_ingredients.deleteMany({
|
||||
where: { recipe_id: id },
|
||||
});
|
||||
|
||||
await this.prisma.recipe_steps.deleteMany({
|
||||
where: { recipe_id: id },
|
||||
});
|
||||
return { message: "Recipe data deleted successfully" };
|
||||
} catch (err) {
|
||||
console.error("Error deleting recipe:", err);
|
||||
logger.error("Error deleting recipe", {
|
||||
message: err instanceof Error ? err.message : "Unknown error",
|
||||
});
|
||||
throw new Error(err instanceof Error ? err.message : "Unknown error");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default RecipeModel;
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ router.get("/recipe/:id", recipeController.getRecipeById);
|
|||
|
||||
router.post("/add-recipe", recipeController.addRecipe);
|
||||
|
||||
router.post("/update-recipe/:id", recipeController.updateRecipe);
|
||||
|
||||
router.post("/set-stars", recipeController.setStars);
|
||||
|
||||
router.delete("/delete-recipe", recipeController.deleteRecipe);
|
||||
|
|
|
|||
|
|
@ -2,9 +2,10 @@ import "./App.css";
|
|||
import Index from "./pages/Index.tsx";
|
||||
import RecipePage from "./pages/RecipePage.tsx";
|
||||
import AddRecipe from "./pages/AddRecipe.tsx";
|
||||
import About from "./pages/About.tsx"
|
||||
import RecipeIngredients from "./pages/RecipeIngredients.tsx"
|
||||
import RecipeSteps from "./pages/RecipeSteps.tsx"
|
||||
import About from "./pages/About.tsx";
|
||||
import RecipeIngredients from "./pages/RecipeIngredients.tsx";
|
||||
import RecipeSteps from "./pages/RecipeSteps.tsx";
|
||||
import EditRecipe from "./pages/EditRecipe.tsx";
|
||||
|
||||
import RecipeBookTabs from "./components/RecipeBookTabs.tsx";
|
||||
import { Routes, Route } from "react-router-dom";
|
||||
|
|
@ -18,6 +19,7 @@ function App() {
|
|||
<Route path="/" element={<Index />} />
|
||||
<Route path="/recipe/:id" element={<RecipePage />} />
|
||||
<Route path="/add-recipe" element={<AddRecipe />} />
|
||||
<Route path="/edit-recipe/:id" element={<EditRecipe />} />
|
||||
<Route path="/about" element={<About />} />
|
||||
<Route path="/recipe-ingredients" element={<RecipeIngredients />} />
|
||||
<Route path="/recipe-steps" element={<RecipeSteps />} />
|
||||
|
|
|
|||
152
frontend/src/components/RecipeForm.tsx
Normal file
152
frontend/src/components/RecipeForm.tsx
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import AddBulkIngredients from "./AddBulkIngredients.tsx";
|
||||
import AddBulkSteps from "./AddBulkSteps.tsx";
|
||||
import StarRating from "./StarRating.tsx";
|
||||
import { isProfane } from "../utils/profanityFilter";
|
||||
import { type Recipe } from "../types/Recipe.ts";
|
||||
|
||||
interface Step {
|
||||
step_number: number;
|
||||
instruction: string;
|
||||
}
|
||||
|
||||
interface RecipeFormProps {
|
||||
onSubmit: (recipeData: Recipe) => Promise<void>;
|
||||
initialData?: Recipe;
|
||||
}
|
||||
|
||||
const RecipeForm: React.FC<RecipeFormProps> = ({ onSubmit, initialData }) => {
|
||||
const [ingredients, setIngredients] = useState<string[]>([]);
|
||||
const [steps, setSteps] = useState<Step[]>([]);
|
||||
const [recipeName, setRecipeName] = useState("");
|
||||
const [recipeCuisine, setRecipeCuisine] = useState("");
|
||||
const [author, setAuthor] = useState("");
|
||||
const [stars, setStars] = useState(0);
|
||||
const [prepMinutes, setPrepMinutes] = useState(5);
|
||||
const [cookMinutes, setCookMinutes] = useState(5);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialData) {
|
||||
setRecipeName(initialData.details.name || "");
|
||||
setRecipeCuisine(initialData.details.cuisine || "");
|
||||
setAuthor(initialData.details.author || "");
|
||||
setStars(initialData.details.stars || 0);
|
||||
setPrepMinutes(initialData.details.prep_minutes || 5);
|
||||
setCookMinutes(initialData.details.cook_minutes || 5);
|
||||
setIngredients(initialData.ingredients);
|
||||
setSteps(initialData.steps);
|
||||
}
|
||||
}, [initialData]);
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (
|
||||
isProfane(recipeName) ||
|
||||
isProfane(recipeCuisine) ||
|
||||
ingredients.some(isProfane) ||
|
||||
steps.some((step) => isProfane(step.instruction)) ||
|
||||
isProfane(author)
|
||||
) {
|
||||
alert("Your submission contains inappropriate language.");
|
||||
return;
|
||||
}
|
||||
|
||||
const recipeData = {
|
||||
details: {
|
||||
name: recipeName,
|
||||
cuisine: recipeCuisine.toLowerCase(),
|
||||
author: author,
|
||||
prep_minutes: prepMinutes,
|
||||
cook_minutes: cookMinutes,
|
||||
stars: stars,
|
||||
},
|
||||
ingredients: ingredients,
|
||||
steps: steps.map((step) => ({
|
||||
id: undefined,
|
||||
step_number: step.step_number,
|
||||
instruction: step.instruction,
|
||||
})),
|
||||
};
|
||||
|
||||
await onSubmit(recipeData);
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="add-recipe-form">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Title"
|
||||
className="recipe-name mb-4 p-2 border border-[var(--color-primaryBorder)] rounded w-full"
|
||||
value={recipeName}
|
||||
onChange={(e) => setRecipeName(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Cuisine"
|
||||
className="recipe-cusine mb-4 p-2 border border-[var(--color-primaryBorder)] rounded w-full"
|
||||
value={recipeCuisine}
|
||||
onChange={(e) => setRecipeCuisine(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Author"
|
||||
className="recipe-cusine mb-4 p-2 border border-[var(--color-primaryBorder)] rounded w-full"
|
||||
value={author}
|
||||
onChange={(e) => setAuthor(e.target.value)}
|
||||
/>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<label
|
||||
htmlFor="prepTime"
|
||||
className="mr-2 font-bold text-[var(--color-secondaryTextDark)]"
|
||||
>
|
||||
Prep Time:
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
placeholder="prep time in minutes"
|
||||
className="recipe-cusine p-2 border border-[var(--color-primaryBorder)] rounded w-24"
|
||||
value={prepMinutes}
|
||||
onChange={(e) => setPrepMinutes(parseInt(e.target.value))}
|
||||
/>
|
||||
<span className="ml-2 text-[var(--color-secondaryTextDark)]">
|
||||
minutes
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="cookTime"
|
||||
className="mr-2 font-bold text-[var(--color-secondaryTextDark)]"
|
||||
>
|
||||
Cook Time:
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
placeholder="cook time in minutes"
|
||||
className="recipe-cusine p-2 border border-[var(--color-primaryBorder)] rounded w-24"
|
||||
value={cookMinutes}
|
||||
onChange={(e) => setCookMinutes(parseInt(e.target.value))}
|
||||
/>
|
||||
<span className="ml-2 text-[var(--color-secondaryTextDark)]">
|
||||
minutes
|
||||
</span>
|
||||
</div>
|
||||
<StarRating
|
||||
rating={stars}
|
||||
onRatingChange={(newRating: number) => setStars(newRating)}
|
||||
/>
|
||||
</div>
|
||||
<AddBulkIngredients ingredients={ingredients} onChange={setIngredients} />
|
||||
<AddBulkSteps steps={steps} onChange={setSteps} />
|
||||
<button
|
||||
type="submit"
|
||||
className="ar-button bg-[var(--color-buttonBg)] text-[var(--color-textLight)] py-2 px-4 rounded hover:bg-[var(--color-buttonBgHover)]"
|
||||
>
|
||||
{initialData ? "Save" : "Add"} Recipe
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default RecipeForm;
|
||||
|
|
@ -1,191 +1,27 @@
|
|||
import React, { useState } from "react";
|
||||
import { addRecipe } from "../services/frontendApi.js";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import AddBulkIngredients from "../components/AddBulkIngredients.tsx";
|
||||
import AddBulkSteps from "../components/AddBulkSteps.tsx";
|
||||
import StarRating from "../components/StarRating.tsx";
|
||||
import RecipeForm from "../components/RecipeForm.tsx";
|
||||
import DemoModal from "../components/DemoModal.tsx";
|
||||
import "../css/colorTheme.css";
|
||||
import { isProfane } from "../utils/profanityFilter";
|
||||
|
||||
interface Step {
|
||||
step_number: number;
|
||||
instruction: string;
|
||||
}
|
||||
import { type Recipe } from "../types/Recipe.ts"
|
||||
|
||||
function AddRecipe() {
|
||||
const [newRecipeId, setNewRecipeId] = useState<number | null>(null);
|
||||
const [ingredients, setIngredients] = useState<string[]>([]);
|
||||
const [steps, setSteps] = useState<Step[]>([]);
|
||||
const [showBulkForm, setShowBulkForm] = useState(true);
|
||||
const [recipeName, setRecipeName] = useState("");
|
||||
const [recipeCuisine, setRecipeCuisine] = useState("");
|
||||
const [author, setAuthor] = useState("");
|
||||
const [stars, setStars] = useState(0);
|
||||
const [prepMinutes, setPrepMinutes] = useState(5);
|
||||
const [cookMinutes, setCookMinutes] = useState(5);
|
||||
const [showDemoModal, setShowDemoModal] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const addRecipeForm = async (event: React.FormEvent) => {
|
||||
event.preventDefault();
|
||||
if (
|
||||
isProfane(recipeName) ||
|
||||
isProfane(recipeCuisine) ||
|
||||
ingredients.some((ingredient) => isProfane(ingredient)) ||
|
||||
steps.some((step) => isProfane(step.instruction)) ||
|
||||
isProfane(author)
|
||||
) {
|
||||
alert("Your submission contains inappropriate language.");
|
||||
return;
|
||||
}
|
||||
const addRecipeForm = async (recipeData: Recipe) => {
|
||||
if (process.env.NODE_ENV === "demo") {
|
||||
setShowDemoModal(true);
|
||||
return;
|
||||
}
|
||||
const stepsHash = Object.fromEntries(
|
||||
steps.map((step) => [step.step_number, step.instruction]),
|
||||
);
|
||||
if (
|
||||
recipeName &&
|
||||
recipeCuisine &&
|
||||
Object.keys(stepsHash).length > 0 &&
|
||||
ingredients.length > 0
|
||||
) {
|
||||
const recipeData = {
|
||||
name: recipeName,
|
||||
cuisine: recipeCuisine.toLowerCase(),
|
||||
author: author,
|
||||
prep_minutes: prepMinutes,
|
||||
cook_minutes: cookMinutes,
|
||||
stars: stars,
|
||||
ingredients: ingredients,
|
||||
steps: stepsHash,
|
||||
};
|
||||
console.log(recipeData);
|
||||
const data = await addRecipe(recipeData);
|
||||
setNewRecipeId(data.id);
|
||||
} else {
|
||||
alert("missing required data");
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (newRecipeId !== null && newRecipeId !== undefined) {
|
||||
navigate(`/recipe/${newRecipeId}`);
|
||||
}
|
||||
}, [newRecipeId, navigate]);
|
||||
const data = await addRecipe(recipeData);
|
||||
navigate(`/recipe/${data.id}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="add-recipe-card page-outer">
|
||||
<form onSubmit={addRecipeForm} className="add-recipe-form">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Title"
|
||||
className="recipe-name mb-4 p-2 border border-[var(--color-primaryBorder)] rounded w-full"
|
||||
value={recipeName}
|
||||
maxLength={35}
|
||||
onChange={(e) => setRecipeName(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Cuisine"
|
||||
className="recipe-cusine mb-4 p-2 border border-[var(--color-primaryBorder)] rounded w-full"
|
||||
value={recipeCuisine}
|
||||
maxLength={15}
|
||||
onChange={(e) => setRecipeCuisine(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Author"
|
||||
className="recipe-cusine mb-4 p-2 border border-[var(--color-primaryBorder)] rounded w-full"
|
||||
value={author}
|
||||
onChange={(e) => setAuthor(e.target.value)}
|
||||
/>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<label
|
||||
htmlFor="prepTime"
|
||||
className="mr-2 font-bold text-[var(--color-secondaryTextDark)]"
|
||||
>
|
||||
Prep Time:
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
placeholder="prep time in minutes"
|
||||
className="recipe-cusine p-2 border border-[var(--color-primaryBorder)] rounded w-24"
|
||||
value={prepMinutes}
|
||||
onChange={(e) => setPrepMinutes(parseInt(e.target.value))}
|
||||
/>
|
||||
<span className="ml-2 text-[var(--color-secondaryTextDark)]">
|
||||
minutes
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="cookTime"
|
||||
className="mr-2 font-bold text-[var(--color-secondaryTextDark)]"
|
||||
>
|
||||
Cook Time:
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
placeholder="cook time in minutes"
|
||||
className="recipe-cusine p-2 border border-[var(--color-primaryBorder)] rounded w-24"
|
||||
value={cookMinutes}
|
||||
onChange={(e) => setCookMinutes(parseInt(e.target.value))}
|
||||
/>
|
||||
<span className="ml-2 text-[var(--color-secondaryTextDark)]">
|
||||
minutes
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<StarRating
|
||||
rating={stars}
|
||||
onRatingChange={(newRating: number) => setStars(newRating)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<label className="mb-4 flex items-center cursor-pointer">
|
||||
<div className="relative">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={showBulkForm}
|
||||
onChange={(e) => setShowBulkForm(e.target.checked)}
|
||||
className="sr-only"
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
<div>
|
||||
<AddBulkIngredients
|
||||
ingredients={ingredients}
|
||||
onChange={setIngredients}
|
||||
/>
|
||||
</div>
|
||||
{/*<ul className="mb-4">
|
||||
{ingredients.map((ing, index) => (
|
||||
<li key={index} className="text-gray-700 flex items-start mb-2">
|
||||
<span>{ing}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>*/}
|
||||
<div>
|
||||
<AddBulkSteps steps={steps} onChange={setSteps} />
|
||||
</div>
|
||||
{/*<ul className="mb-4">
|
||||
{steps.map((step) => (
|
||||
<li key={step.step_number} className="text-gray-700 flex items-start mb-2">
|
||||
<span>{`${step.step_number}. ${step.instruction}`}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>*/}
|
||||
<button
|
||||
type="submit"
|
||||
className="ar-button bg-[var(--color-buttonBg)] text-[var(--color-textLight)] py-2 px-4 rounded hover:bg-[var(--color-buttonBgHover)]"
|
||||
>
|
||||
submit
|
||||
</button>
|
||||
</form>
|
||||
<RecipeForm onSubmit={addRecipeForm} />
|
||||
{showDemoModal && (
|
||||
<DemoModal
|
||||
isOpen={showDemoModal}
|
||||
|
|
@ -196,4 +32,5 @@ function AddRecipe() {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AddRecipe;
|
||||
|
|
|
|||
50
frontend/src/pages/EditRecipe.tsx
Normal file
50
frontend/src/pages/EditRecipe.tsx
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import { useParams, useNavigate } from "react-router-dom";
|
||||
import { getRecipeById, updateRecipe } from "../services/frontendApi.js";
|
||||
import RecipeForm from "../components/RecipeForm.tsx";
|
||||
import DemoModal from "../components/DemoModal.tsx";
|
||||
import { type Recipe } from "../types/Recipe.ts"
|
||||
|
||||
function EditRecipe() {
|
||||
const { id } = useParams();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [recipe, setRecipe] = useState<Recipe>();
|
||||
const [showDemoModal, setShowDemoModal] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchRecipe = async () => {
|
||||
const recipeData = await getRecipeById(id);
|
||||
setRecipe(recipeData);
|
||||
};
|
||||
|
||||
fetchRecipe();
|
||||
}, [id]);
|
||||
|
||||
const updateRecipeForm = async (recipeData: Recipe) => {
|
||||
if (process.env.NODE_ENV === "demo") {
|
||||
setShowDemoModal(true);
|
||||
return;
|
||||
}
|
||||
|
||||
await updateRecipe(id, recipeData);
|
||||
navigate(`/recipe/${id}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="add-recipe-card page-outer">
|
||||
{recipe && (
|
||||
<RecipeForm onSubmit={updateRecipeForm} initialData={recipe} />
|
||||
)}
|
||||
{showDemoModal && (
|
||||
<DemoModal
|
||||
isOpen={showDemoModal}
|
||||
onClose={() => setShowDemoModal(false)}
|
||||
closeModal={() => setShowDemoModal(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditRecipe;
|
||||
|
|
@ -111,15 +111,30 @@ function RecipePage() {
|
|||
<div className="border-b-2 border-[var(--color-primaryBorder)] pb-4">
|
||||
<div className="recipe-card">
|
||||
<div className="flex relative justify-between">
|
||||
<div className="invisible-buttons">
|
||||
<button
|
||||
onClick={() => { }}
|
||||
onClick={() => {}}
|
||||
className="invisible ar-button py-1 px-1 rounded mr-2 self-start"
|
||||
>
|
||||
🗑️
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {}}
|
||||
className="invisible ar-button py-1 px-1 rounded self-start"
|
||||
>
|
||||
🗑️
|
||||
</button>
|
||||
</div>
|
||||
<h3 className="text-center max-w-lg px-4 text-2xl lg:text-3xl font-bold text-[var(--color-textDark)]">
|
||||
{recipe.details.name}
|
||||
</h3>
|
||||
<div className="modify-buttons">
|
||||
<button
|
||||
onClick={() => navigate(`/edit-recipe/${recipe.details.id}`)}
|
||||
className="ar-button bg-[var(--color-buttonBg)] text-[var(--color-textLight)] py-1 px-1 rounded hover:bg-[var(--color-buttonBgHover)] mr-2 self-start"
|
||||
>
|
||||
🔧
|
||||
</button>
|
||||
<button
|
||||
onClick={openModal}
|
||||
className="ar-button bg-[var(--color-buttonBg)] text-[var(--color-textLight)] py-1 px-1 rounded hover:bg-[var(--color-buttonBgHover)] self-start"
|
||||
|
|
@ -127,6 +142,7 @@ function RecipePage() {
|
|||
🗑️
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-1">
|
||||
<p className="text-[var(--color-textDark)] italic text-lg">
|
||||
{recipe.details.cuisine}
|
||||
|
|
@ -146,7 +162,10 @@ function RecipePage() {
|
|||
</h4>
|
||||
<ul className="space-y-2">
|
||||
{recipe.ingredients.map((ingredient: string, index) => (
|
||||
<li key={index} className="text-[var(--color-secondaryTextDark)] flex items-start">
|
||||
<li
|
||||
key={index}
|
||||
className="text-[var(--color-secondaryTextDark)] flex items-start"
|
||||
>
|
||||
<span className="w-1.5 h-1.5 bg-[var(--color-buttonBg)] rounded-full mt-2 mr-3 flex-shrink-0"></span>
|
||||
<span className="font-medium text-left">{ingredient}</span>
|
||||
</li>
|
||||
|
|
|
|||
|
|
@ -42,6 +42,18 @@ export const addRecipe = async (recipeData) => {
|
|||
return data;
|
||||
};
|
||||
|
||||
export const updateRecipe = async (id, recipeData) => {
|
||||
console.log("updateRecipe");
|
||||
console.log(recipeData);
|
||||
const response = await fetch(`${baseUrl}/update-recipe/${id}`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(recipeData),
|
||||
});
|
||||
const data = await response.json();
|
||||
return data;
|
||||
};
|
||||
|
||||
export const setDBStars = async (id, stars) => {
|
||||
console.log(JSON.stringify({ id: id, stars: stars }));
|
||||
// return
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
interface Step {
|
||||
id: number;
|
||||
id?: number;
|
||||
step_number: number;
|
||||
instruction: string;
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@ interface Recipe {
|
|||
ingredients: string[];
|
||||
steps: Step[];
|
||||
}
|
||||
|
||||
// smaller Recipe type returned by backend at /recipes for all
|
||||
interface RecipeSmall {
|
||||
id: number;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue