edit recipe

This commit is contained in:
fred 2025-10-20 11:43:29 -07:00
parent 798e879863
commit 8b52d31dcf
10 changed files with 389 additions and 217 deletions

View file

@ -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 />} />

View 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;

View file

@ -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;

View 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;

View file

@ -111,21 +111,37 @@ function RecipePage() {
<div className="border-b-2 border-[var(--color-primaryBorder)] pb-4">
<div className="recipe-card">
<div className="flex relative justify-between">
<button
onClick={() => { }}
className="invisible ar-button py-1 px-1 rounded self-start"
>
🗑
</button>
<div className="invisible-buttons">
<button
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>
<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 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"
>
🗑
</button>
</div>
</div>
<div className="mt-1">
<p className="text-[var(--color-textDark)] italic text-lg">
@ -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>

View file

@ -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

View file

@ -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;