From 63d00494502954bd833a9f329b5800d8cf35d94a Mon Sep 17 00:00:00 2001 From: Mikayla Dobson <93477693+innocuous-symmetry@users.noreply.github.com> Date: Sun, 19 Feb 2023 12:04:46 -0600 Subject: [PATCH] in progress: associate ingredients to recipe on save --- .../components/derived/IngredientSelector.tsx | 14 +++-- client/src/components/pages/AddRecipe.tsx | 53 ++++++++++++++++++- client/src/util/API.ts | 12 ++--- server/controllers/RecipeCtl.ts | 13 ++++- server/models/recipe.ts | 22 +++++++- server/routes/ingredient.ts | 2 + server/routes/recipe.ts | 10 +++- server/schemas/index.ts | 5 ++ 8 files changed, 116 insertions(+), 15 deletions(-) diff --git a/client/src/components/derived/IngredientSelector.tsx b/client/src/components/derived/IngredientSelector.tsx index 6f48b43..88da14f 100644 --- a/client/src/components/derived/IngredientSelector.tsx +++ b/client/src/components/derived/IngredientSelector.tsx @@ -1,7 +1,7 @@ import { Autocomplete, TextField } from "@mui/material" import { ChangeEvent, useEffect, useRef, useState } from "react"; import { useAuthContext } from "../../context/AuthContext"; -import { DropdownData, IIngredient } from "../../schemas"; +import { DropdownData, IIngredient, RecipeIngredient } from "../../schemas"; import { IngredientFieldData } from "../../util/types"; import { Button } from "../ui"; @@ -17,7 +17,7 @@ const createIngredient = (name: string, userid: string | number) => { return { name: name, createdbyid: userid - } as IIngredient + } } const quantityOptions: readonly any[] = [ @@ -149,7 +149,15 @@ function IngredientSelector({ position, ingredients, units, getRowState, destroy if (typeof newValue == 'string') { const newIngredient = createIngredient(newValue, user.id!); - setIngredientOptions((prev) => [...prev, newValue]); + + setIngredientOptions((prev) => { + let shouldInsert = true; + for (let each of prev) { + if (each == newValue) shouldInsert = false; + } + + return (shouldInsert ? [...prev, newValue] : prev); + }); setRowState((prev) => { let shouldInsert = true; diff --git a/client/src/components/pages/AddRecipe.tsx b/client/src/components/pages/AddRecipe.tsx index 1687f84..c81874a 100644 --- a/client/src/components/pages/AddRecipe.tsx +++ b/client/src/components/pages/AddRecipe.tsx @@ -130,9 +130,17 @@ export default function AddRecipe() { const handleCreate = async () => { if (!user || !token) return; + // initialize API handlers + const recipeAPI = new API.Recipe(token); + const ingredientAPI = new API.Ingredient(token); + + // array to aggregate error/success messages + let messages = new Array(); + // inject current user id into recipe entry setInput({ ...input, authoruserid: user.id! }); + // verify all required fields are set for (let field of Object.keys(input)) { // account for an edge case where this state may not have been set yet if (field == 'authoruserid' as keyof IRecipe) { @@ -140,16 +148,57 @@ export default function AddRecipe() { } if (!input[field as keyof IRecipe]) { + messages.push("Missing required field " + field); return; } } - const recipe = new API.Recipe(token); - const result = await recipe.post(input); + let ingredientSelections = new Array(); + let newIngredientCount = 0; + // handle ingredient row data + for (let row of ingredientFieldData) { + if (row.ingredientSelection === undefined) { + messages.push("Please ensure you have included ingredient selections for all rows."); + } + + ingredientSelections.push(row.ingredientSelection); + + for (let ing of row.ingredients) { + // filter out recipes that already exist + if (ingredients.filter(x => x.name == ing.name).includes(ing)) { + continue; + } + + // update the ingredient list + setIngredients((prev) => [...prev, ing]); + + // post the new ingredient to the database + const newEntry = await ingredientAPI.post(ing); + messages.push(`Successfully created new ingredient: ${ing.name}!`); + console.log(newEntry); + newIngredientCount++; + } + } + + // post recipe entry + const result = await recipeAPI.post(input); + + // handle recipe post resolve/reject if (result) { const recipeID = result.recipe.id; const recipeName = result.recipe.name; + let recipeIngredientCount = 0; + + for (let ing of ingredientSelections) { + const ok = await recipeAPI.addIngredientToRecipe(ing, recipeID); + if (ok) recipeIngredientCount++; + } + + messages.push(`Created recipe ${recipeName} with ${recipeIngredientCount} total ingredients!`) + if (newIngredientCount > 0) { + messages.push(`Successfully created ${newIngredientCount} new ingredients! Thanks for helping us grow.`); + } setToast( diff --git a/client/src/util/API.ts b/client/src/util/API.ts index 1e8c8bc..f95fe88 100644 --- a/client/src/util/API.ts +++ b/client/src/util/API.ts @@ -1,5 +1,5 @@ import { AxiosError, AxiosHeaders, AxiosRequestHeaders, AxiosResponse } from "axios"; -import { IUser, IUserAuth, IFriendship, IRecipe, IIngredient, ICollection, IGroceryList, DropdownData } from "../schemas"; +import { IUser, IUserAuth, IFriendship, IRecipe, IIngredient, ICollection, IGroceryList, DropdownData, RecipeIngredient } from "../schemas"; import { default as _instance } from "./axiosInstance"; module API { @@ -187,6 +187,11 @@ module API { constructor(token: string) { super(Settings.getAPISTRING() + "/app/recipe", token); } + + async addIngredientToRecipe(ingredient: RecipeIngredient, recipeid: string | number) { + const response = await this.instance.post(this.endpoint + `?addIngredients=true&recipeID=${recipeid}`, JSON.stringify(ingredient), this.headers); + return Promise.resolve(response.data); + } } export class Ingredient extends RestController { @@ -194,11 +199,6 @@ module API { super(Settings.getAPISTRING() + "/app/ingredient", token); } - async associateIngredientWithRecipe(recipeID: string | number, ingredientID: string | number) { - const response = await this.instance.post(this.endpoint + `/${ingredientID}?recipeID=${recipeID}`, this.headers); - return Promise.resolve(response.data); - } - async getAllForRecipe(recipeID: string | number) { const response = await this.instance.get(this.endpoint + `?recipeID=${recipeID}`, this.headers); return Promise.resolve(response.data); diff --git a/server/controllers/RecipeCtl.ts b/server/controllers/RecipeCtl.ts index c3f9980..685d391 100644 --- a/server/controllers/RecipeCtl.ts +++ b/server/controllers/RecipeCtl.ts @@ -1,4 +1,4 @@ -import { IRecipe } from "../schemas"; +import { IRecipe, RecipeIngredient } from "../schemas"; import { Recipe } from "../models/recipe"; import ControllerResponse from "../util/ControllerResponse"; import { StatusCode } from "../util/types"; @@ -58,4 +58,15 @@ export default class RecipeCtl { throw new Error(error); } } + + async addIngredientToRecipe(ingredient: RecipeIngredient, recipeid: string | number) { + try { + const result = await RecipeInstance.addIngredientToRecipe(ingredient, recipeid); + const ok = result !== null; + const code = ok ? StatusCode.NewContent : StatusCode.BadRequest; + return new ControllerResponse(code, (result || "Something went wrong")); + } catch (error: any) { + throw new Error(error); + } + } } \ No newline at end of file diff --git a/server/models/recipe.ts b/server/models/recipe.ts index d35fd53..c057b31 100644 --- a/server/models/recipe.ts +++ b/server/models/recipe.ts @@ -1,4 +1,4 @@ -import { IRecipe } from "../schemas"; +import { IIngredient, IRecipe, RecipeIngredient } from "../schemas"; import fs from 'fs'; import pool from "../db"; import { CollectionCtl } from "../controllers"; @@ -114,4 +114,24 @@ export class Recipe { throw new Error(error); } } + + async addIngredientToRecipe(ingredient: RecipeIngredient, recipeid: string | number) { + const { quantity, unit, id } = ingredient; + + try { + const statement = ` + INSERT INTO recipin.cmp_recipeingredient + (quantity, unit, ingredientid, recipeid) + VALUES ($1, $2, $3, $4) RETURNING * + ` + + const result = await pool.query(statement, [quantity, unit, id, recipeid]); + + if (result.rows) return result.rows[0]; + return []; + } catch (e: any) { + throw new Error(e); + } + + } } \ No newline at end of file diff --git a/server/routes/ingredient.ts b/server/routes/ingredient.ts index 3d8a010..88a3c12 100644 --- a/server/routes/ingredient.ts +++ b/server/routes/ingredient.ts @@ -51,4 +51,6 @@ export const ingredientRoute = (app: Express) => { next(e); } }) + + return router; } \ No newline at end of file diff --git a/server/routes/recipe.ts b/server/routes/recipe.ts index 13a4d1f..a0e6c87 100644 --- a/server/routes/recipe.ts +++ b/server/routes/recipe.ts @@ -57,10 +57,16 @@ export const recipeRoute = (app: Express) => { router.post('/', restrictAccess, async (req, res, next) => { const user = req.user as IUser; const data = req.body; + const { addIngredients, recipeID } = req.query; try { - const result = await recipectl.post(user.id as number, data); - res.status(result.code).send(result.data); + if (addIngredients) { + const result = await recipectl.addIngredientToRecipe(data, recipeID as string); + res.status(result.code).send(result.data); + } else { + const result = await recipectl.post(user.id as number, data); + res.status(result.code).send(result.data); + } } catch(e) { next(e); } diff --git a/server/schemas/index.ts b/server/schemas/index.ts index db53027..bfb7c42 100644 --- a/server/schemas/index.ts +++ b/server/schemas/index.ts @@ -44,6 +44,11 @@ export interface IIngredient extends HasHistory { createdbyid: string | number } +export interface RecipeIngredient extends Partial { + unit: string + quantity: string | number +} + export interface ICollection extends HasHistory, CanDeactivate { name: string ismaincollection: boolean