From 6f43d17ddd0d40ed8962974d11b143387eed41f0 Mon Sep 17 00:00:00 2001 From: fred <> Date: Thu, 24 Jul 2025 12:11:32 -0700 Subject: [PATCH] recipe author and stars and a bit of cleanup --- backend/.gitignore | 1 + backend/backendServer.js | 86 +++++++++++-------- ...0250717181149_add_raw_ingredients_table.js | 24 ++++++ ...50724163016_add_author_stars_to_recipes.js | 22 +++++ frontend/src/App.css | 2 +- frontend/src/App.tsx | 4 + .../src/components/AddBulkIngredients.tsx | 30 +------ frontend/src/components/AddBulkSteps.tsx | 12 ++- .../src/components/CookbookRecipeTile.tsx | 8 +- frontend/src/components/Modal.tsx | 10 +-- frontend/src/components/RecipeBookTabs.tsx | 1 - frontend/src/components/StarRating.tsx | 26 ++++++ frontend/src/css/Modal.css | 23 ----- frontend/src/css/Navbar.css | 47 ---------- frontend/src/pages/AddRecipe.tsx | 69 +++++++-------- frontend/src/pages/Index.tsx | 11 +-- frontend/src/pages/RecipeIngredients.tsx | 48 +++++++++++ frontend/src/pages/RecipePage.tsx | 45 ++++++++-- frontend/src/pages/RecipeSteps.tsx | 47 ++++++++++ frontend/src/services/frontendApi.js | 27 +++++- frontend/src/types/Recipe.ts | 25 ++++-- 21 files changed, 361 insertions(+), 207 deletions(-) create mode 100644 backend/migrations/20250717181149_add_raw_ingredients_table.js create mode 100644 backend/migrations/20250724163016_add_author_stars_to_recipes.js create mode 100644 frontend/src/components/StarRating.tsx delete mode 100644 frontend/src/css/Modal.css delete mode 100644 frontend/src/css/Navbar.css create mode 100644 frontend/src/pages/RecipeIngredients.tsx create mode 100644 frontend/src/pages/RecipeSteps.tsx diff --git a/backend/.gitignore b/backend/.gitignore index 3c3629e..1cfa4bd 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -1 +1,2 @@ node_modules +scratch diff --git a/backend/backendServer.js b/backend/backendServer.js index 41b5549..d998d67 100644 --- a/backend/backendServer.js +++ b/backend/backendServer.js @@ -22,17 +22,34 @@ app.get("/recipes", async (req, res) => { res.status(500).json({ error: err.message }); } }); +// ### GET ALL RECIPE_INGREDIENTS ### +app.get("/recipe-ingredients", async (req, res) => { + try { + const recipe_ingredients = await db('recipe_ingredients').select('*'); + res.json(recipe_ingredients); + } catch (err) { + console.log(err); + res.status(500).json({ error: err.message }); + } +}); +// ### GET ALL RECIPE_STEPS ### +app.get("/recipe-steps", async (req, res) => { + try { + const recipe_steps = await db('recipe_steps').select('*'); + res.json(recipe_steps); + } catch (err) { + console.log(err); + res.status(500).json({ error: err.message }); + } +}); // ### GET RECIPE ### app.get("/recipe/:id", async (req, res) => { const id = req.params.id try { - const recipeQuery = db('recipes').where('id', '=', id).select('id', 'name', 'cuisine'); + const recipeQuery = db('recipes').where('id', '=', id).select('id', 'name', 'author', 'cuisine', 'stars'); - const ingredientsQuery = db.from('recipe_ingredients as ri') - .join('ingredients as i', 'ri.ingredient_id', 'i.id') - .where('ri.recipe_id', id) - .select('i.name', 'ri.quantity', 'ri.unit'); + const ingredientsQuery = db.from('recipe_ingredients').where('recipe_id', '=', id).select('raw'); const stepsQuery = db('recipe_steps').where('recipe_id', id).select('step_number', 'instruction'); @@ -40,15 +57,8 @@ app.get("/recipe/:id", async (req, res) => { const result = { details: recipe[0], - ingredients: ingredients.map(ingredient => ({ - name: ingredient.name, - quantity: ingredient.quantity, - unit: ingredient.unit - })), - steps: steps.reduce((acc, step) => { - acc[step.step_number] = step.instruction; - return acc; - }, {}) + ingredients: ingredients, + steps: steps }; res.json(result); @@ -60,38 +70,22 @@ app.get("/recipe/:id", async (req, res) => { // ### ADD RECIPE ### app.post("/add-recipe", async (req, res) => { - const { name, cuisine, ingredients, steps } = req.body; + const { name, author, cuisine, stars, ingredients, steps } = req.body; try { const [id] = await db('recipes').insert({ name: name, - cuisine: cuisine + author: author, + cuisine: cuisine, + stars: stars }, ['id']) - const existingIngredients = await db('ingredients').whereIn('name', ingredients.map(ing => ing.name)); - let ingredientData = []; - for (let ingredient of ingredients) { - const existingIngredient = existingIngredients.find(ing => ing.name === ingredient.name); - if (!existingIngredient) { - // create the ingredient if there is no entry - const [newIngredient] = await db('ingredients').insert({ - name: ingredient.name - }, ['id']); - ingredientData.push({ id: newIngredient.id, quantity: ingredient.quantity, unit: ingredient.unit }); - } else { - // if the ingredient exists use existing entry - ingredientData.push({ id: existingIngredient.id, quantity: ingredient.quantity, unit: ingredient.unit }); - } - } - - const ingredientInserts = ingredientData.map(ing => ({ - ingredient_id: ing.id, - quantity: ing.quantity, - unit: ing.unit, - recipe_id: id.id + const ingredientInserts = ingredients.map(ing => ({ + recipe_id: id.id, + raw: ing })); + // await db('recipe_ingredients').insert(ingredientInserts); - // Step 4: Insert steps into recipe_steps const stepInserts = Object.keys(steps).map(stepNumber => ({ recipe_id: id.id, step_number: parseInt(stepNumber), @@ -106,10 +100,26 @@ app.post("/add-recipe", async (req, res) => { } }); +// ### SET STARS ### +app.post("/set-stars", async (req, res) => { + const { id, stars } = req.body; + try { + await db('recipes') + .where({ id: id }) + .update({ stars: stars }) + res.status(200).send({ message: "stars updated" }) + } catch (err) { + console.log(err); + res.status(500).json({ error: err.message }); + } +}); + // ### DELETE RECIPE ### app.delete("/delete-recipe", async (req, res) => { const { id } = req.body; try { + await db('recipe_steps').where({ recipe_id: id }).del(); + await db('recipe_ingredients').where({ recipe_id: id }).del(); await db('recipes').where({ id: id }).del(); res.status(200).send({ message: "Recipe deleted" }); } catch (err) { diff --git a/backend/migrations/20250717181149_add_raw_ingredients_table.js b/backend/migrations/20250717181149_add_raw_ingredients_table.js new file mode 100644 index 0000000..7a15863 --- /dev/null +++ b/backend/migrations/20250717181149_add_raw_ingredients_table.js @@ -0,0 +1,24 @@ +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.up = function (knex) { + return knex.schema.table('recipe_ingredients', table => { + table.string('raw', 255).defaultTo(''); + table.integer('ingredient_id').nullable().alter(); + table.string('quantity').nullable().alter(); + table.string('unit').nullable().alter(); + }).table('recipe_steps', table => { + table.string('instruction', 510).alter(); + }); +}; + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.down = function (knex) { + return knex.schema.table('recipe_ingredients', table => { + table.dropColumn('raw'); + }); +}; diff --git a/backend/migrations/20250724163016_add_author_stars_to_recipes.js b/backend/migrations/20250724163016_add_author_stars_to_recipes.js new file mode 100644 index 0000000..5c5aa10 --- /dev/null +++ b/backend/migrations/20250724163016_add_author_stars_to_recipes.js @@ -0,0 +1,22 @@ +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.up = function (knex) { + + return knex.schema.table('recipes', table => { + table.string('author'); + table.integer('stars'); + }) +}; + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.down = function (knex) { + return knex.schema.table('recipes', table => { + table.dropColumn('author'); + table.dropColumn('stars') + }) +}; diff --git a/frontend/src/App.css b/frontend/src/App.css index b342a6f..3dfd428 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -3,7 +3,7 @@ @tailwind utilities; #root { - @apply mx-auto text-center; + @apply mx-auto text-center max-w-screen-lg; } .page-outer { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 1ef37fe..a15c4a9 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -3,6 +3,8 @@ 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 RecipeBookTabs from "./components/RecipeBookTabs.tsx"; import { Routes, Route } from "react-router-dom"; @@ -17,6 +19,8 @@ function App() { } /> } /> } /> + } /> + } /> diff --git a/frontend/src/components/AddBulkIngredients.tsx b/frontend/src/components/AddBulkIngredients.tsx index df75336..49968bb 100644 --- a/frontend/src/components/AddBulkIngredients.tsx +++ b/frontend/src/components/AddBulkIngredients.tsx @@ -1,44 +1,22 @@ import React, { useState, useEffect } from 'react'; -import { type Ingredient } from "../types/Recipe"; interface AddBulkIngredientsProps { - ingredients: Ingredient[]; - onChange?: (ingredients: Ingredient[]) => void; + ingredients: string[]; + onChange?: (ingredients: string[]) => void; } const AddBulkIngredients: React.FC = ({ ingredients, onChange }) => { const [textValue, setTextValue] = useState(''); useEffect(() => { - const textRepresentation = ingredients.map(ingredient => - `${ingredient.quantity} ${ingredient.unit} ${ingredient.name}` - ).join('\n'); + const textRepresentation = ingredients.join('\n'); setTextValue(textRepresentation); }, [ingredients]); const parseAndUpdate = (value: string) => { const lines = value.split('\n').filter(line => line.trim() !== ''); - const pattern = /^([0-9/.]+)?\s*(\S+)\s*((\w+\s*)*)$/; - const parsedIngredients = lines.map(line => { - const parts = line.match(pattern); - let quantity = 0; - if (parts?.[1]) { - const [num, denom] = parts[1].split('/'); - if (denom) { - quantity = parseFloat(num) / parseFloat(denom); - } else { - quantity = parseFloat(parts[1]); - } - } - return { - quantity: +quantity.toFixed(2), - unit: parts?.[2]?.trim() || '', - name: parts?.[3]?.trim() || '' - }; - }); - - if (onChange) onChange(parsedIngredients); + if (onChange) onChange(lines); }; const handleInputChange = (e: React.ChangeEvent) => { diff --git a/frontend/src/components/AddBulkSteps.tsx b/frontend/src/components/AddBulkSteps.tsx index 1263852..cb1b373 100644 --- a/frontend/src/components/AddBulkSteps.tsx +++ b/frontend/src/components/AddBulkSteps.tsx @@ -1,5 +1,9 @@ import React, { useState, useEffect } from 'react'; -import { type Step } from "../types/Recipe"; + +interface Step { + step_number: number; + instruction: string; +} interface AddBulkStepsProps { steps: Step[]; @@ -11,7 +15,7 @@ const AddBulkSteps: React.FC = ({ steps, onChange }) => { useEffect(() => { const textRepresentation = steps.map(step => - `${step.instructions}` + `${step.instruction}` ).join('\n'); setTextValue(textRepresentation); }, [steps]); @@ -24,8 +28,8 @@ const AddBulkSteps: React.FC = ({ steps, onChange }) => { const parseAndUpdate = (value: string) => { const lines = value.split('\n').filter(line => line.trim() !== ''); - const parsedSteps = lines.map((line, idx) => { - return { idx: idx + 1, instructions: line } + const parsedSteps: Step[] = lines.map((line, idx) => { + return { step_number: idx + 1, instruction: line } }) if (onChange) onChange(parsedSteps); diff --git a/frontend/src/components/CookbookRecipeTile.tsx b/frontend/src/components/CookbookRecipeTile.tsx index 4128e7d..9dd12e6 100644 --- a/frontend/src/components/CookbookRecipeTile.tsx +++ b/frontend/src/components/CookbookRecipeTile.tsx @@ -1,17 +1,17 @@ import React, { useState } from 'react'; -import { type Recipe } from "../types/Recipe" +import { type RecipeSmall } from "../types/Recipe" import Modal from '../components/Modal.tsx' interface CookbookRecipeTileProps { - recipe: Recipe; + recipe: RecipeSmall; handleDelete: (id: number | undefined) => void; } function CookbookRecipeTile({ recipe, handleDelete }: CookbookRecipeTileProps) { const [isModalOpen, setIsModalOpen] = useState(false); - const openModal = () => setIsModalOpen(true); - const closeModal = () => setIsModalOpen(false); + const openModal = () => { setIsModalOpen(true) }; + const closeModal = () => { setIsModalOpen(false) }; const confirmDelete = () => { handleDelete(recipe.id); closeModal(); diff --git a/frontend/src/components/Modal.tsx b/frontend/src/components/Modal.tsx index 446cd94..8985471 100644 --- a/frontend/src/components/Modal.tsx +++ b/frontend/src/components/Modal.tsx @@ -1,5 +1,3 @@ -import "../css/Modal.css" - interface ModalProps { isOpen: boolean; onClose: () => void; @@ -12,14 +10,14 @@ const Modal = ({ isOpen, onClose, message, confirmAction, cancelAction }: ModalP if (!isOpen) return null; return ( -
-
e.stopPropagation()}> +
+
e.stopPropagation()}>
{message}
- - + +
diff --git a/frontend/src/components/RecipeBookTabs.tsx b/frontend/src/components/RecipeBookTabs.tsx index f5d8b26..c78c6d7 100644 --- a/frontend/src/components/RecipeBookTabs.tsx +++ b/frontend/src/components/RecipeBookTabs.tsx @@ -1,4 +1,3 @@ -import React, { useState } from 'react'; import { Link, useLocation } from 'react-router-dom'; const RecipeBookTabs = () => { diff --git a/frontend/src/components/StarRating.tsx b/frontend/src/components/StarRating.tsx new file mode 100644 index 0000000..c2a0865 --- /dev/null +++ b/frontend/src/components/StarRating.tsx @@ -0,0 +1,26 @@ +interface StarRatingProps { + rating: number; + onRatingChange: (newRating: number) => void; +} + +const StarRating = ({ rating, onRatingChange }: StarRatingProps) => { + + return ( +
+ {[...Array(5)].map((star, index) => { + index += 1; + return ( + onRatingChange(index)} + style={{ color: index <= rating ? 'gold' : 'gray', fontSize: '2rem', cursor: 'pointer' }} + > + ★ + + ); + })} +
+ ); +}; + +export default StarRating; diff --git a/frontend/src/css/Modal.css b/frontend/src/css/Modal.css deleted file mode 100644 index f9ba8a3..0000000 --- a/frontend/src/css/Modal.css +++ /dev/null @@ -1,23 +0,0 @@ -.modal-overlay { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(0, 0, 0, 0.5); - display: flex; - justify-content: center; - align-items: center; -} - -.modal-content { - background: darkblue; - padding: 50px; - border-radius: 5px; - box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); -} - -.modal-content button { - background: gray; - margin: 1em; -} diff --git a/frontend/src/css/Navbar.css b/frontend/src/css/Navbar.css deleted file mode 100644 index cba96cd..0000000 --- a/frontend/src/css/Navbar.css +++ /dev/null @@ -1,47 +0,0 @@ -.navbar { - background-color: #000000; - padding: 1rem 2rem; - display: flex; - justify-content: space-between; - align-items: center; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); -} - -.navbar-brand { - font-size: 1.5rem; - font-weight: bold; -} - -.navbar-links { - display: flex; - gap: 2rem; -} - -.nav-link { - font-size: 1rem; - padding: 0.5rem 1rem; - border-radius: 4px; - transition: background-color 0.2s; -} - -.nav-link:hover { - background-color: rgba(255, 255, 255, 0.1); -} - -@media (max-width: 768px) { - .navbar { - padding: 1rem; - } - - .navbar-brand { - font-size: 1.2rem; - } - - .navbar-links { - gap: 1rem; - } - - .nav-link { - padding: 0.5rem; - } -} \ No newline at end of file diff --git a/frontend/src/pages/AddRecipe.tsx b/frontend/src/pages/AddRecipe.tsx index 62624a3..0bfede3 100644 --- a/frontend/src/pages/AddRecipe.tsx +++ b/frontend/src/pages/AddRecipe.tsx @@ -2,29 +2,37 @@ import React, { useState } from 'react'; import { addRecipe } from "../services/frontendApi.js"; import { useNavigate } from "react-router-dom"; import AddBulkIngredients from "../components/AddBulkIngredients.tsx" -import AddIngredientsForm from "../components/AddIngredientsForm.tsx" -import AddStepsForm from "../components/AddStepsForm.tsx" import AddBulkSteps from "../components/AddBulkSteps.tsx" -import { type Ingredient, type Step } from "../types/Recipe"; +import StarRating from "../components/StarRating.tsx" +// import { type Step } from "../types/Recipe"; + +interface Step { + step_number: number; + instruction: string; +} function AddRecipe() { const [newRecipeId, setNewRecipeId] = useState(null); const navigate = useNavigate(); - const [ingredients, setIngredients] = useState([]); + const [ingredients, setIngredients] = useState([]); const [steps, setSteps] = useState([]); const [showBulkForm, setShowBulkForm] = useState(true); const [recipeName, setRecipeName] = useState(""); const [recipeCuisine, setRecipeCuisine] = useState(""); + const [author, setAuthor] = useState(""); + const [stars, setStars] = useState(0); const addRecipeForm = async (event: React.FormEvent) => { event.preventDefault(); const stepsHash = Object.fromEntries( - steps.map(step => [step.idx, step.instructions]) + 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, + author: author, + stars: stars, ingredients: ingredients, steps: stepsHash } @@ -47,7 +55,7 @@ function AddRecipe() {
setRecipeName(e.target.value)} @@ -59,6 +67,16 @@ function AddRecipe() { value={recipeCuisine} onChange={(e) => setRecipeCuisine(e.target.value)} /> + setAuthor(e.target.value)} + /> +
+ setStars(newRating)} /> +
@@ -71,50 +89,29 @@ function AddRecipe() { onChange={(e) => setShowBulkForm(e.target.checked)} className="sr-only" /> -
-
-
Bulk Entry
- {showBulkForm ? - : - - } +
-
    + {/*
      {ingredients.map((ing, index) => (
    • - {`${ing.quantity} ${ing.unit} ${ing.name}`} + {ing}
    • ))} -
    +
*/}
- {showBulkForm ? - : - - } +
-
    + {/*
      {steps.map((step) => ( -
    • - {`${step.idx}. ${step.instructions}`} +
    • + {`${step.step_number}. ${step.instruction}`}
    • ))} -
    +
*/}
) } diff --git a/frontend/src/pages/Index.tsx b/frontend/src/pages/Index.tsx index 6afcbc3..70ab1c4 100644 --- a/frontend/src/pages/Index.tsx +++ b/frontend/src/pages/Index.tsx @@ -1,14 +1,13 @@ import { useState, useEffect } from "react"; import CookbookRecipeTile from "../components/CookbookRecipeTile.tsx" import { getRecipes, deleteRecipe } from "../services/frontendApi.js"; - - +import { type RecipeSmall } from "../types/Recipe.ts" function AllRecipes() { const [searchQuery, setSearchQuery] = useState(""); - const [recipes, setRecipes] = useState([]); - const [cuisines, setCuisines] = useState([]); + const [recipes, setRecipes] = useState([]); + const [cuisines, setCuisines] = useState([]); const [error, setError] = useState(null); const [loading, setLoading] = useState(true); const [shouldFetchRecipes, setShouldFetchRecipes] = useState(true); @@ -20,7 +19,9 @@ function AllRecipes() { const recipes = await getRecipes(); setRecipes(recipes); console.log(recipes) - const uniqueCuisines: [] = Array.from(new Set(recipes.map(recipe => recipe.cuisine))) + const uniqueCuisines: string[] = recipes.length > 0 + ? Array.from(new Set(recipes.map((recipe: RecipeSmall) => recipe.cuisine))) + : []; setCuisines(uniqueCuisines) console.log(cuisines) } catch (error) { diff --git a/frontend/src/pages/RecipeIngredients.tsx b/frontend/src/pages/RecipeIngredients.tsx new file mode 100644 index 0000000..bbdf549 --- /dev/null +++ b/frontend/src/pages/RecipeIngredients.tsx @@ -0,0 +1,48 @@ +import { getRecipeIngredients } from "../services/frontendApi.js"; +import { useState, useEffect } from "react"; +import { type Ingredient } from "../types/Recipe.ts" + +function RecipeIngredients() { + + const [recipeIngredients, setRecipeIngredients] = useState([]); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const loadRecipeIngredients = async () => { + try { + const recipeIngredients = await getRecipeIngredients(); + setRecipeIngredients(recipeIngredients); + console.log(recipeIngredients) + } catch (err) { + console.log(err); + setError("Failed to load recipe ingredients..."); + console.log(error) + } finally { + setLoading(false); + } + }; + loadRecipeIngredients(); + }, []); + console.log(recipeIngredients) + return ( + // should this be a string[]? only if we are only returning raw. otherwise i will need to type and return the ingredient object. This template shoudl work for steps though, so maybe setting that up is a good first step +
+ {loading ? ( +
Loading...
+ ) : ( +
+
+ {recipeIngredients.map(ing => ( +
  • + {ing.raw} +
  • + ))} +
    +
    + )} +
    + ) + +} +export default RecipeIngredients diff --git a/frontend/src/pages/RecipePage.tsx b/frontend/src/pages/RecipePage.tsx index 94ba10d..6893ce7 100644 --- a/frontend/src/pages/RecipePage.tsx +++ b/frontend/src/pages/RecipePage.tsx @@ -2,22 +2,31 @@ import { useParams } from "react-router-dom"; import { useState, useEffect } from "react"; import { getRecipeById } from "../services/frontendApi.js"; import { type Recipe, type Ingredient } from "../types/Recipe" +import StarRating from "../components/StarRating.tsx" +import { setDBStars } from "../services/frontendApi.js"; function RecipePage() { const [recipe, setRecipe] = useState({ details: {}, ingredients: [], - steps: {} + steps: [] }); const [error, setError] = useState(null); const [loading, setLoading] = useState(true); const { id } = useParams(); + const isWebSource = recipe && recipe.details && recipe.details.author + ? /http|com/.test(recipe.details.author) //etc + : false; + const [stars, setStars] = useState(0); + const [initialStars, setInitialStars] = useState(null); useEffect(() => { const loadRecipe = async () => { try { const recipe = await getRecipeById(id); setRecipe(recipe); + setStars(recipe.details?.stars ?? 0) + setInitialStars(recipe.details?.stars ?? 0); console.log(recipe) } catch (error) { console.log(error); @@ -28,7 +37,21 @@ function RecipePage() { }; loadRecipe(); }, [id]); + + useEffect(() => { + if (initialStars === null || initialStars === stars) { + return; + } + + const updateStarsInDB = async () => { + await setDBStars(id, stars); + }; + + updateStarsInDB(); + }, [stars]); console.log(recipe) + console.log(stars) + console.log(initialStars) return (
    @@ -40,11 +63,11 @@ function RecipePage() {
    -

    {recipe.details.name}

    +

    {recipe.details.name}

    {recipe.details.cuisine}

    -
    +

    @@ -54,7 +77,7 @@ function RecipePage() { {recipe.ingredients.map((ingredient: Ingredient, index) => (
  • - {ingredient.quantity} {ingredient.unit} {ingredient.name} + {ingredient.raw}
  • ))} @@ -69,9 +92,9 @@ function RecipePage() { {recipe.steps && Object.keys(recipe.steps || {}).map((stepNumber) => (
  • - {stepNumber} + {recipe.steps[parseInt(stepNumber)].step_number} - {recipe.steps[parseInt(stepNumber)]} + {recipe.steps[parseInt(stepNumber)].instruction}
  • ))} @@ -80,8 +103,14 @@ function RecipePage() {
    - From the Kitchen of - ★ ★ ★ + {isWebSource ? ( + Source: {recipe.details.author} + ) : ( + From the kitchen of {recipe.details.author} + )} + + setStars(newRating)} /> +

    diff --git a/frontend/src/pages/RecipeSteps.tsx b/frontend/src/pages/RecipeSteps.tsx new file mode 100644 index 0000000..af87737 --- /dev/null +++ b/frontend/src/pages/RecipeSteps.tsx @@ -0,0 +1,47 @@ +import { getRecipeSteps } from "../services/frontendApi.js"; +import { useState, useEffect } from "react"; +import { type Step } from "../types/Recipe.ts" + +function RecipeSteps() { + + const [recipeSteps, setRecipeSteps] = useState([]); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const loadRecipeSteps = async () => { + try { + const recipeSteps = await getRecipeSteps(); + setRecipeSteps(recipeSteps); + console.log(recipeSteps) + } catch (err) { + console.log(err); + setError("Failed to load recipe ingredients..."); + console.log(error) + } finally { + setLoading(false); + } + }; + loadRecipeSteps(); + }, []); + console.log(recipeSteps) + return ( +
    + {loading ? ( +
    Loading...
    + ) : ( +
    +
    + {recipeSteps.map(step => ( +
  • + {step.instruction} +
  • + ))} +
    +
    + )} +
    + ) + +} +export default RecipeSteps diff --git a/frontend/src/services/frontendApi.js b/frontend/src/services/frontendApi.js index 728b9bb..bd0cd53 100644 --- a/frontend/src/services/frontendApi.js +++ b/frontend/src/services/frontendApi.js @@ -4,6 +4,18 @@ export const getRecipes = async () => { return data; }; +export const getRecipeSteps = async () => { + const response = await fetch("http://localhost:3000/recipe-steps"); + const data = await response.json(); + return data; +}; + +export const getRecipeIngredients = async () => { + const response = await fetch("http://localhost:3000/recipe-ingredients"); + const data = await response.json(); + return data; +}; + export const getRecipeById = async (id) => { const response = await fetch(`http://localhost:3000/recipe/${id}`); const data = await response.json(); @@ -19,10 +31,23 @@ export const addRecipe = async (recipeData) => { body: JSON.stringify(recipeData) }); const data = await response.json(); - console.log(data) + console.log(data); return data; }; +export const setDBStars = async (id, stars) => { + console.log(JSON.stringify({ id: id, stars: stars })) + // return + const response = await fetch("http://localhost:3000/set-stars", { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ id: id, stars: stars }) + }); + const data = await response.json(); + console.log(data); + return data; +} + export const deleteRecipe = async (id) => { console.log(id) // return diff --git a/frontend/src/types/Recipe.ts b/frontend/src/types/Recipe.ts index f000607..08f48b0 100644 --- a/frontend/src/types/Recipe.ts +++ b/frontend/src/types/Recipe.ts @@ -1,22 +1,33 @@ interface Step { - idx: number; - instructions: string; + id: number; + step_number: number; + instruction: string; } interface Ingredient { - name: string; - quantity: number; - unit: string; + id?: number; + name?: string; + quantity?: number; + unit?: string; + raw?: string; } interface Recipe { details: { id?: number; name?: string; + author?: string; + stars?: number; cuisine?: string; }, ingredients: Ingredient[], - steps?: Step[]; + steps: Step[]; +} +// smaller Recipe type returned by backend at /recipes for all +interface RecipeSmall { + id: number; + name: string; + cuisine: string; } -export type { Recipe, Ingredient, Step } +export type { Recipe, Ingredient, Step, RecipeSmall }