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;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
console.log(req.body);
|
||||||
const createdRecipe = await model.addRecipe(req.body);
|
const createdRecipe = await model.addRecipe(req.body);
|
||||||
res.status(201).json(createdRecipe);
|
res.status(201).json(createdRecipe);
|
||||||
} catch (error) {
|
} 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> => {
|
export const setStars = async (req: Request, res: Response): Promise<void> => {
|
||||||
if (process.env.NODE_ENV === "demo") {
|
if (process.env.NODE_ENV === "demo") {
|
||||||
return;
|
return;
|
||||||
|
|
@ -104,6 +126,7 @@ export default {
|
||||||
getAllRecipes,
|
getAllRecipes,
|
||||||
getRecipeById,
|
getRecipeById,
|
||||||
addRecipe,
|
addRecipe,
|
||||||
|
updateRecipe,
|
||||||
setStars,
|
setStars,
|
||||||
deleteRecipe,
|
deleteRecipe,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ class RecipeModel {
|
||||||
|
|
||||||
async getAllRecipes(): Promise<any[]> {
|
async getAllRecipes(): Promise<any[]> {
|
||||||
try {
|
try {
|
||||||
logger.info("index page view")
|
logger.info("index page view");
|
||||||
return await this.prisma.recipes.findMany();
|
return await this.prisma.recipes.findMany();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error fetching all recipes:", err);
|
console.error("Error fetching all recipes:", err);
|
||||||
|
|
@ -46,7 +46,10 @@ class RecipeModel {
|
||||||
instruction: step.instruction,
|
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;
|
return data;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log("Error finding recipe:", err);
|
console.log("Error finding recipe:", err);
|
||||||
|
|
@ -58,25 +61,25 @@ class RecipeModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
async addRecipe(recipeData: {
|
async addRecipe(recipeData: {
|
||||||
name: string;
|
details: {
|
||||||
author: string;
|
name: string;
|
||||||
cuisine: string;
|
author: string;
|
||||||
stars: number;
|
cuisine: string;
|
||||||
|
stars: number;
|
||||||
|
prep_minutes: number;
|
||||||
|
cook_minutes: number;
|
||||||
|
};
|
||||||
ingredients: string[];
|
ingredients: string[];
|
||||||
steps: { [key: string]: string };
|
steps: { [key: string]: string };
|
||||||
prep_minutes: number;
|
|
||||||
cook_minutes: number;
|
|
||||||
}): Promise<any> {
|
}): Promise<any> {
|
||||||
const {
|
const name = recipeData.details.name;
|
||||||
name,
|
const author = recipeData.details.author;
|
||||||
author,
|
const cuisine = recipeData.details.cuisine;
|
||||||
cuisine,
|
const stars = recipeData.details.stars;
|
||||||
stars,
|
const prep_minutes = recipeData.details.prep_minutes;
|
||||||
ingredients,
|
const cook_minutes = recipeData.details.cook_minutes;
|
||||||
steps,
|
const ingredients = recipeData.ingredients;
|
||||||
prep_minutes,
|
const steps = recipeData.steps;
|
||||||
cook_minutes,
|
|
||||||
} = recipeData;
|
|
||||||
try {
|
try {
|
||||||
const createdRecipe = await this.prisma.recipes.create({
|
const createdRecipe = await this.prisma.recipes.create({
|
||||||
data: {
|
data: {
|
||||||
|
|
@ -90,10 +93,7 @@ class RecipeModel {
|
||||||
create: ingredients.map((ing) => ({ raw: ing })),
|
create: ingredients.map((ing) => ({ raw: ing })),
|
||||||
},
|
},
|
||||||
recipeSteps: {
|
recipeSteps: {
|
||||||
create: Object.keys(steps).map((stepNumber) => ({
|
create: steps,
|
||||||
step_number: parseInt(stepNumber),
|
|
||||||
instruction: steps[stepNumber],
|
|
||||||
})),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -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 }> {
|
async setStars(id: number, stars: number): Promise<{ message: string }> {
|
||||||
try {
|
try {
|
||||||
await this.prisma.recipes.update({
|
await this.prisma.recipes.update({
|
||||||
|
|
@ -130,13 +191,7 @@ class RecipeModel {
|
||||||
|
|
||||||
async deleteRecipe(id: number): Promise<{ message: string }> {
|
async deleteRecipe(id: number): Promise<{ message: string }> {
|
||||||
try {
|
try {
|
||||||
await this.prisma.recipe_ingredients.deleteMany({
|
this.deleteRecipeData(id);
|
||||||
where: { recipe_id: id },
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.prisma.recipe_steps.deleteMany({
|
|
||||||
where: { recipe_id: id },
|
|
||||||
});
|
|
||||||
const deletedRecipe = await this.prisma.recipes.delete({
|
const deletedRecipe = await this.prisma.recipes.delete({
|
||||||
where: { id },
|
where: { id },
|
||||||
});
|
});
|
||||||
|
|
@ -153,6 +208,25 @@ class RecipeModel {
|
||||||
throw new Error(err instanceof Error ? err.message : "Unknown error");
|
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;
|
export default RecipeModel;
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@ router.get("/recipe/:id", recipeController.getRecipeById);
|
||||||
|
|
||||||
router.post("/add-recipe", recipeController.addRecipe);
|
router.post("/add-recipe", recipeController.addRecipe);
|
||||||
|
|
||||||
|
router.post("/update-recipe/:id", recipeController.updateRecipe);
|
||||||
|
|
||||||
router.post("/set-stars", recipeController.setStars);
|
router.post("/set-stars", recipeController.setStars);
|
||||||
|
|
||||||
router.delete("/delete-recipe", recipeController.deleteRecipe);
|
router.delete("/delete-recipe", recipeController.deleteRecipe);
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,10 @@ import "./App.css";
|
||||||
import Index from "./pages/Index.tsx";
|
import Index from "./pages/Index.tsx";
|
||||||
import RecipePage from "./pages/RecipePage.tsx";
|
import RecipePage from "./pages/RecipePage.tsx";
|
||||||
import AddRecipe from "./pages/AddRecipe.tsx";
|
import AddRecipe from "./pages/AddRecipe.tsx";
|
||||||
import About from "./pages/About.tsx"
|
import About from "./pages/About.tsx";
|
||||||
import RecipeIngredients from "./pages/RecipeIngredients.tsx"
|
import RecipeIngredients from "./pages/RecipeIngredients.tsx";
|
||||||
import RecipeSteps from "./pages/RecipeSteps.tsx"
|
import RecipeSteps from "./pages/RecipeSteps.tsx";
|
||||||
|
import EditRecipe from "./pages/EditRecipe.tsx";
|
||||||
|
|
||||||
import RecipeBookTabs from "./components/RecipeBookTabs.tsx";
|
import RecipeBookTabs from "./components/RecipeBookTabs.tsx";
|
||||||
import { Routes, Route } from "react-router-dom";
|
import { Routes, Route } from "react-router-dom";
|
||||||
|
|
@ -18,6 +19,7 @@ function App() {
|
||||||
<Route path="/" element={<Index />} />
|
<Route path="/" element={<Index />} />
|
||||||
<Route path="/recipe/:id" element={<RecipePage />} />
|
<Route path="/recipe/:id" element={<RecipePage />} />
|
||||||
<Route path="/add-recipe" element={<AddRecipe />} />
|
<Route path="/add-recipe" element={<AddRecipe />} />
|
||||||
|
<Route path="/edit-recipe/:id" element={<EditRecipe />} />
|
||||||
<Route path="/about" element={<About />} />
|
<Route path="/about" element={<About />} />
|
||||||
<Route path="/recipe-ingredients" element={<RecipeIngredients />} />
|
<Route path="/recipe-ingredients" element={<RecipeIngredients />} />
|
||||||
<Route path="/recipe-steps" element={<RecipeSteps />} />
|
<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 React, { useState } from "react";
|
||||||
import { addRecipe } from "../services/frontendApi.js";
|
import { addRecipe } from "../services/frontendApi.js";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import AddBulkIngredients from "../components/AddBulkIngredients.tsx";
|
import RecipeForm from "../components/RecipeForm.tsx";
|
||||||
import AddBulkSteps from "../components/AddBulkSteps.tsx";
|
|
||||||
import StarRating from "../components/StarRating.tsx";
|
|
||||||
import DemoModal from "../components/DemoModal.tsx";
|
import DemoModal from "../components/DemoModal.tsx";
|
||||||
import "../css/colorTheme.css";
|
import { type Recipe } from "../types/Recipe.ts"
|
||||||
import { isProfane } from "../utils/profanityFilter";
|
|
||||||
|
|
||||||
interface Step {
|
|
||||||
step_number: number;
|
|
||||||
instruction: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function AddRecipe() {
|
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 [showDemoModal, setShowDemoModal] = useState(false);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const addRecipeForm = async (event: React.FormEvent) => {
|
const addRecipeForm = async (recipeData: Recipe) => {
|
||||||
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;
|
|
||||||
}
|
|
||||||
if (process.env.NODE_ENV === "demo") {
|
if (process.env.NODE_ENV === "demo") {
|
||||||
setShowDemoModal(true);
|
setShowDemoModal(true);
|
||||||
return;
|
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(() => {
|
const data = await addRecipe(recipeData);
|
||||||
if (newRecipeId !== null && newRecipeId !== undefined) {
|
navigate(`/recipe/${data.id}`);
|
||||||
navigate(`/recipe/${newRecipeId}`);
|
};
|
||||||
}
|
|
||||||
}, [newRecipeId, navigate]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="add-recipe-card page-outer">
|
<div className="add-recipe-card page-outer">
|
||||||
<form onSubmit={addRecipeForm} className="add-recipe-form">
|
<RecipeForm onSubmit={addRecipeForm} />
|
||||||
<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>
|
|
||||||
{showDemoModal && (
|
{showDemoModal && (
|
||||||
<DemoModal
|
<DemoModal
|
||||||
isOpen={showDemoModal}
|
isOpen={showDemoModal}
|
||||||
|
|
@ -196,4 +32,5 @@ function AddRecipe() {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AddRecipe;
|
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,21 +111,37 @@ function RecipePage() {
|
||||||
<div className="border-b-2 border-[var(--color-primaryBorder)] pb-4">
|
<div className="border-b-2 border-[var(--color-primaryBorder)] pb-4">
|
||||||
<div className="recipe-card">
|
<div className="recipe-card">
|
||||||
<div className="flex relative justify-between">
|
<div className="flex relative justify-between">
|
||||||
<button
|
<div className="invisible-buttons">
|
||||||
onClick={() => { }}
|
<button
|
||||||
className="invisible ar-button py-1 px-1 rounded self-start"
|
onClick={() => {}}
|
||||||
>
|
className="invisible ar-button py-1 px-1 rounded mr-2 self-start"
|
||||||
🗑️
|
>
|
||||||
</button>
|
🗑️
|
||||||
|
</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)]">
|
<h3 className="text-center max-w-lg px-4 text-2xl lg:text-3xl font-bold text-[var(--color-textDark)]">
|
||||||
{recipe.details.name}
|
{recipe.details.name}
|
||||||
</h3>
|
</h3>
|
||||||
<button
|
<div className="modify-buttons">
|
||||||
onClick={openModal}
|
<button
|
||||||
className="ar-button bg-[var(--color-buttonBg)] text-[var(--color-textLight)] py-1 px-1 rounded hover:bg-[var(--color-buttonBgHover)] self-start"
|
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>
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
🗑️
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1">
|
<div className="mt-1">
|
||||||
<p className="text-[var(--color-textDark)] italic text-lg">
|
<p className="text-[var(--color-textDark)] italic text-lg">
|
||||||
|
|
@ -146,7 +162,10 @@ function RecipePage() {
|
||||||
</h4>
|
</h4>
|
||||||
<ul className="space-y-2">
|
<ul className="space-y-2">
|
||||||
{recipe.ingredients.map((ingredient: string, index) => (
|
{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="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>
|
<span className="font-medium text-left">{ingredient}</span>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,18 @@ export const addRecipe = async (recipeData) => {
|
||||||
return data;
|
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) => {
|
export const setDBStars = async (id, stars) => {
|
||||||
console.log(JSON.stringify({ id: id, stars: stars }));
|
console.log(JSON.stringify({ id: id, stars: stars }));
|
||||||
// return
|
// return
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
interface Step {
|
interface Step {
|
||||||
id: number;
|
id?: number;
|
||||||
step_number: number;
|
step_number: number;
|
||||||
instruction: string;
|
instruction: string;
|
||||||
}
|
}
|
||||||
|
|
@ -17,6 +17,7 @@ interface Recipe {
|
||||||
ingredients: string[];
|
ingredients: string[];
|
||||||
steps: Step[];
|
steps: Step[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// smaller Recipe type returned by backend at /recipes for all
|
// smaller Recipe type returned by backend at /recipes for all
|
||||||
interface RecipeSmall {
|
interface RecipeSmall {
|
||||||
id: number;
|
id: number;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue