From 8a939e6a818e23c536ed566f2d658f657cd99ede Mon Sep 17 00:00:00 2001 From: Mikayla Dobson <93477693+innocuous-symmetry@users.noreply.github.com> Date: Sat, 25 Feb 2023 20:21:29 -0600 Subject: [PATCH] in progress: handle add ingredients to recipe --- client/src/App.tsx | 5 +- client/src/components/derived/Friends.tsx | 7 +- client/src/components/pages/AddRecipe.tsx | 81 +++++++++++++++-------- client/src/components/pages/Recipe.tsx | 26 +++++--- client/src/components/ui/Browser.tsx | 3 + client/src/components/ui/index.ts | 4 +- client/src/util/API.ts | 80 ++++++++++++++-------- client/src/util/axiosInstance.ts | 4 +- dev.sh | 2 + package.json | 24 +++++++ server/controllers/IngredientCtl.ts | 11 +++ server/models/ingredient.ts | 11 +++ server/routes/ingredient.ts | 11 ++- server/routes/recipe.ts | 2 + 14 files changed, 195 insertions(+), 76 deletions(-) create mode 100644 dev.sh create mode 100644 package.json diff --git a/client/src/App.tsx b/client/src/App.tsx index 62389e5..29b5da5 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -2,8 +2,6 @@ import { useEffect } from 'react'; import { BrowserRouter, Routes, Route } from 'react-router-dom'; import { useAuthContext } from './context/AuthContext'; -import jwtDecode from 'jwt-decode'; -import API from './util/API'; // pages, ui, components, styles import Subscriptions from './components/pages/Subscriptions/Subscriptions'; @@ -19,11 +17,10 @@ import CollectionBrowser from './components/pages/CollectionBrowser'; import { Navbar } from './components/ui'; import GroceryList from './components/pages/GroceryList'; import GroceryListCollection from './components/pages/GroceryListCollection'; -import { TokenType } from './util/types'; -import './sass/App.scss'; import handleToken from './util/handleToken'; import AddFriends from './components/pages/AddFriends'; import Sandbox from './components/Sandbox'; +import './sass/App.scss'; function App() { const { setUser, token, setToken } = useAuthContext(); diff --git a/client/src/components/derived/Friends.tsx b/client/src/components/derived/Friends.tsx index fcd1a87..7c00c35 100644 --- a/client/src/components/derived/Friends.tsx +++ b/client/src/components/derived/Friends.tsx @@ -1,7 +1,6 @@ +import { useAuthContext } from "../../context/AuthContext"; import { FC, useEffect, useState } from "react"; import { v4 } from "uuid"; -import { useAuthContext } from "../../context/AuthContext"; -import { getAllUsers, getFriendships, getPendingFriendRequests, getUserByID } from "../../util/apiUtils"; import API from "../../util/API"; import UserCard from "../ui/UserCard"; import { IUser, IFriendship } from "../../schemas"; @@ -18,9 +17,9 @@ const Friends: FC<{ targetUser?: IUser }> = ({ targetUser }) => { (async function() { try { const Friends = new API.Friendship(token); - const result = await Friends.getAll(); + const result: IFriendship[] | null = await Friends.getAll(); - if (result.length) { + if (result?.length) { setFriends(result); } diff --git a/client/src/components/pages/AddRecipe.tsx b/client/src/components/pages/AddRecipe.tsx index c81874a..ab9097a 100644 --- a/client/src/components/pages/AddRecipe.tsx +++ b/client/src/components/pages/AddRecipe.tsx @@ -1,15 +1,18 @@ -import { useAuthContext } from "../../context/AuthContext"; +// library/framework import { useCallback, useEffect, useState } from "react"; -import { Button, Card, Divider, Page, Panel } from "../ui" -import { DropdownData, IIngredient, IRecipe } from "../../schemas"; -import IngredientSelector from "../derived/IngredientSelector"; +import { Autocomplete, TextField } from "@mui/material"; +import { v4 } from "uuid"; + +// util/api +import { useAuthContext } from "../../context/AuthContext"; +import { DropdownData, IIngredient, IRecipe, RecipeIngredient } from "../../schemas"; +import { IngredientFieldData } from "../../util/types"; import Protect from "../../util/Protect"; import API from "../../util/API"; -import { v4 } from "uuid"; -import RichText from "../ui/RichText"; -import { Autocomplete, TextField } from "@mui/material"; -import { IngredientFieldData } from "../../util/types"; -import Toast from "../ui/Toast"; + +// ui/components +import { Button, Card, Divider, Panel, RichText, Toast } from "../ui" +import IngredientSelector from "../derived/IngredientSelector"; export default function AddRecipe() { /********************************** @@ -103,7 +106,7 @@ export default function AddRecipe() { }); } - if (courseList) { + if (courseList && !courseData.length) { setCourseData((prev) => { let newEntries = filterDuplicateEntries(courseList, prev); return [...prev, ...newEntries]; @@ -112,22 +115,23 @@ export default function AddRecipe() { })(); }, [token]) - // mount the ingredient selection section once dependencies have loaded + // mount the ingredient selection section once conditions met: + // 1. ingredients have been fetched + // 2. measurements have been fetched + // 3. ingredient fields have not already been initialized useEffect(() => { - if (ingredients.length && measurements.length) { + const conditionsMet = (ingredients.length && measurements.length) && (!ingredientFields.length); + + if (conditionsMet) { setIngredientFields([]); } }, [ingredients, measurements]) - useEffect(() => { - console.log(ingredientFieldData); - }, [getRowState]); - /********************************** * PAGE SPECIFIC FUNCTIONS *********************************/ // submit handler - const handleCreate = async () => { + async function handleCreate() { if (!user || !token) return; // initialize API handlers @@ -153,31 +157,52 @@ export default function AddRecipe() { } } - let ingredientSelections = new Array(); + let preparedIngredientData = new Array(); let newIngredientCount = 0; // handle ingredient row data for (let row of ingredientFieldData) { + if (!row) continue; + console.log(row); + if (row.ingredientSelection === undefined) { messages.push("Please ensure you have included ingredient selections for all rows."); + continue; } - ingredientSelections.push(row.ingredientSelection); + if (!row.quantity) { + messages.push("Please provide a quantity for ingredient " + row.ingredientSelection); + continue; + } + + if (!row.measurement) { + messages.push(row.ingredientSelection + " missing required unit of measurement"); + continue; + } + + const newID = row.ingredients.filter(ingr => ingr.name == row.ingredientSelection)[0].id; + + const newIngredientData: RecipeIngredient = { + id: newID ?? row.ingredients.length + 1, + name: row.ingredientSelection as string, + quantity: row.quantity, + unit: row.measurement + } + + preparedIngredientData.push(newIngredientData); for (let ing of row.ingredients) { // filter out recipes that already exist - if (ingredients.filter(x => x.name == ing.name).includes(ing)) { - continue; + if (!ingredients.filter(x => x.name == ing.name).includes(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++; } // 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++; } } @@ -190,7 +215,7 @@ export default function AddRecipe() { const recipeName = result.recipe.name; let recipeIngredientCount = 0; - for (let ing of ingredientSelections) { + for (let ing of preparedIngredientData) { const ok = await recipeAPI.addIngredientToRecipe(ing, recipeID); if (ok) recipeIngredientCount++; } diff --git a/client/src/components/pages/Recipe.tsx b/client/src/components/pages/Recipe.tsx index fc950cb..e0b7974 100644 --- a/client/src/components/pages/Recipe.tsx +++ b/client/src/components/pages/Recipe.tsx @@ -1,12 +1,13 @@ import { useEffect, useState } from "react"; import { useParams } from "react-router-dom"; import { Divider, Page, Panel } from "../ui"; -import { IRecipe, IUser, IIngredient } from "../../schemas"; +import { IRecipe, IUser, IIngredient, RecipeIngredient } from "../../schemas"; import { getRecipeByID } from "../../util/apiUtils"; import Protect from "../../util/Protect"; import API from "../../util/API"; import { useAuthContext } from "../../context/AuthContext"; import ResourceNotFound from "./StatusPages/404"; +import { v4 } from "uuid"; export default function Recipe() { const { user, token } = useAuthContext(); @@ -14,7 +15,7 @@ export default function Recipe() { const [recipe, setRecipe] = useState(); const [userData, setUserData] = useState(); - const [ingredientData, setIngredientData] = useState([]); + const [ingredientData, setIngredientData] = useState([]); const [view, setView] = useState(Loading...); useEffect(() => { @@ -32,17 +33,19 @@ export default function Recipe() { }, [token]) useEffect(() => { - if (recipe) { + if (recipe && id) { if (recipe === "no recipe") { setView(We couldn't find a recipe with the ID {id}.); } else { if (!user || !token) return; - (async() => { + // while ingredient data has not yet been mapped, + // get ingredients and map them + (!ingredientData.length) && (async() => { const ingredientAPI = new API.Ingredient(token); - const result = await ingredientAPI.getAllForRecipe(id!); + const result = await ingredientAPI.getAllForRecipe(id); if (result.length) setIngredientData(result); - }) + })(); const selfAuthored = (recipe.authoruserid == user.id!); if (selfAuthored) { @@ -61,7 +64,7 @@ export default function Recipe() { useEffect(() => { if (!userData) return; - if (recipe && recipe !== "no recipe") { + if (recipe && ingredientData && recipe !== "no recipe") { setView( {recipe.name} @@ -75,8 +78,11 @@ export default function Recipe() { Ingredients: { ingredientData.length - ? ingredientData.map((each: IIngredient) => {each.name}) - : "No ingredients for this recipe" + ? ingredientData.map((each: RecipeIngredient) => ( + + {each.quantity} {each.quantity == 1 ? each.unit : (each.unit + "s")} {each.name} + + )) : No ingredients for this recipe } @@ -86,7 +92,7 @@ export default function Recipe() { ); } - }, [userData, ingredientData]); + }, [userData, recipe, ingredientData]); return view } \ No newline at end of file diff --git a/client/src/components/ui/Browser.tsx b/client/src/components/ui/Browser.tsx index ec23f34..d0149bb 100644 --- a/client/src/components/ui/Browser.tsx +++ b/client/src/components/ui/Browser.tsx @@ -1,4 +1,5 @@ import { FC, useEffect, useState } from "react"; +import { useAuthContext } from "../../context/AuthContext"; import Protect from "../../util/Protect"; import Form from "./Form/Form"; @@ -9,6 +10,8 @@ interface BrowserProps { } const Browser: FC = ({ children, header, searchFunction }) => { + const { user, token } = useAuthContext(); + const [form, setForm] = useState(); useEffect(() => { diff --git a/client/src/components/ui/index.ts b/client/src/components/ui/index.ts index 0f753d7..19db3c2 100644 --- a/client/src/components/ui/index.ts +++ b/client/src/components/ui/index.ts @@ -6,10 +6,12 @@ import Form from "./Form/Form"; import Navbar from "./Navbar"; import Page from "./Page"; import Panel from "./Panel"; +import RichText from "./RichText"; import TextField from "./TextField"; +import Toast from "./Toast"; import Tooltip from "./Tooltip"; import UserCard from "./UserCard"; export { - Button, Card, Dropdown, Divider, Form, Navbar, Page, Panel, TextField, Tooltip, UserCard + Button, Card, Dropdown, Divider, Form, Navbar, Page, Panel, RichText, TextField, Toast, Tooltip, UserCard } \ No newline at end of file diff --git a/client/src/util/API.ts b/client/src/util/API.ts index f95fe88..a80255e 100644 --- a/client/src/util/API.ts +++ b/client/src/util/API.ts @@ -56,29 +56,66 @@ module API { } async getAll() { - const response = await this.instance.get(this.endpoint, this.headers); - return Promise.resolve(response.data); + const response = await this.instance.get(this.endpoint, this.headers) + .catch((err: AxiosError) => { + console.log(err.message); + return null; + }); + return Promise.resolve(response?.data); } async getByID(id: string) { - const response = await this.instance.get(this.endpoint + "/" + id, this.headers); - return Promise.resolve(response.data); + const response = await this.instance.get(this.endpoint + "/" + id, this.headers) + .catch((err: AxiosError) => { + console.log(err.message); + return null; + }); + return Promise.resolve(response?.data); } async post(data: T) { - console.log(data); - const response = await this.instance.post(this.endpoint, data, this.headers); - return Promise.resolve(response.data); + try { + const response = await this.instance.post(this.endpoint, JSON.stringify(data), this.headers); + return Promise.resolve(response.data); + } catch(e: any) { + if (e instanceof AxiosError) { + const error = e as AxiosError; + if (error.message) { + console.log(error.message); + return null; + } + } + } } async put(id: string, data: T | Partial) { - const response = await this.instance.put(this.endpoint + "/" + id, JSON.stringify(data), this.headers); - return Promise.resolve(response.data); + try { + const response = await this.instance.put(this.endpoint + "/" + id, JSON.stringify(data), this.headers); + return Promise.resolve(response.data); + } catch(e: any) { + if (e instanceof AxiosError) { + const error = e as AxiosError; + if (error.message) { + console.log(error.message); + return null; + } + } + } } async delete(id: string) { - const response = await this.instance.delete(this.endpoint + '/' + id, this.headers); - return Promise.resolve(response.data); + try { + const response = await this.instance.delete(this.endpoint + '/' + id, this.headers); + return Promise.resolve(response.data); + } catch(e: any) { + if (e instanceof AxiosError) { + const error = e as AxiosError; + if (error.message) { + console.log(error.message); + return null; + } + } + } } } @@ -141,19 +178,6 @@ module API { super(Settings.getAPISTRING() + "/app/friend", token); } - override async getAll() { - try { - const response = await this.instance.get(this.endpoint, this.headers); - return Promise.resolve(response.data); - } catch(e) { - const error = e as AxiosError; - if (error.response?.status == 404) { - console.log('no friends found'); - return []; - } - } - } - async getTargetUserFriendships(id: string | number) { try { const response = await this.instance.get(this.endpoint + `?targetUser=${id}`, this.headers); @@ -189,8 +213,12 @@ module API { } 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); + const response = await this.instance.post(this.endpoint + `?addIngredients=true&recipeID=${recipeid}`, JSON.stringify(ingredient), this.headers) + .catch((err) => { + console.log(err); + return null; + }); + return Promise.resolve(response?.data); } } diff --git a/client/src/util/axiosInstance.ts b/client/src/util/axiosInstance.ts index 5e8c3fb..b74e680 100644 --- a/client/src/util/axiosInstance.ts +++ b/client/src/util/axiosInstance.ts @@ -14,7 +14,9 @@ instance.interceptors.response.use((res: AxiosResponse) => { return res; }, (err) => { - return Promise.reject(err); + console.log(err); + // return err; + // return Promise.reject(err); }) export default instance; \ No newline at end of file diff --git a/dev.sh b/dev.sh new file mode 100644 index 0000000..abaf628 --- /dev/null +++ b/dev.sh @@ -0,0 +1,2 @@ +#! /bin/bash +concurrently "cd server && npm run dev" "cd client && npm run dev" \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..cd9a5df --- /dev/null +++ b/package.json @@ -0,0 +1,24 @@ +{ + "name": "recipe-manager", + "version": "1.0.0", + "description": "A concept developed by Mikayla Dobson", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "dev": "bash dev.sh" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/innocuous-symmetry/recipe-manager.git" + }, + "keywords": [], + "author": "", + "license": "ISC", + "bugs": { + "url": "https://github.com/innocuous-symmetry/recipe-manager/issues" + }, + "homepage": "https://github.com/innocuous-symmetry/recipe-manager#readme", + "devDependencies": { + "concurrently": "^7.6.0" + } +} diff --git a/server/controllers/IngredientCtl.ts b/server/controllers/IngredientCtl.ts index 04ae1d6..6b1db57 100644 --- a/server/controllers/IngredientCtl.ts +++ b/server/controllers/IngredientCtl.ts @@ -16,6 +16,17 @@ export default class IngredientCtl { } } + async getAllForRecipe(recipeid: string) { + try { + const result = await IngredientInstance.getAllForRecipe(recipeid); + const ok = result !== null; + const code = ok ? StatusCode.OK : StatusCode.NotFound; + return new ControllerResponse(code, (result || "No ingredient found with this recipe ID")); + } catch (e: any) { + throw new Error(e); + } + } + async getOne(id: string) { try { const result = await IngredientInstance.getOne(id); diff --git a/server/models/ingredient.ts b/server/models/ingredient.ts index 5b1002a..6d2a653 100644 --- a/server/models/ingredient.ts +++ b/server/models/ingredient.ts @@ -25,6 +25,17 @@ export class Ingredient { } } + async getAllForRecipe(recipeid: string) { + try { + const statement = `SELECT * FROM recipin.cmp_recipeingredient WHERE recipeid = $1`; + const result = await pool.query(statement, [recipeid]); + if (result.rows.length) return result.rows[0]; + return null; + } catch (e: any) { + throw new Error(e); + } + } + async post(data: IIngredient) { try { const statement = ` diff --git a/server/routes/ingredient.ts b/server/routes/ingredient.ts index 88a3c12..1aa0355 100644 --- a/server/routes/ingredient.ts +++ b/server/routes/ingredient.ts @@ -10,9 +10,16 @@ export const ingredientRoute = (app: Express) => { app.use('/app/ingredient', router); router.get('/', async (req, res, next) => { + const { recipeID } = req.query; + try { - const result: CtlResponse = await IngredientInstance.getAll(); - res.status(result.code).send(result.data); + if (recipeID) { + const result = await IngredientInstance.getAllForRecipe(recipeID as string); + res.status(result.code).send(result.data); + } else { + const result: CtlResponse = await IngredientInstance.getAll(); + res.status(result.code).send(result.data); + } } catch(e) { next(e); } diff --git a/server/routes/recipe.ts b/server/routes/recipe.ts index a0e6c87..769c43f 100644 --- a/server/routes/recipe.ts +++ b/server/routes/recipe.ts @@ -59,6 +59,8 @@ export const recipeRoute = (app: Express) => { const data = req.body; const { addIngredients, recipeID } = req.query; + console.log(data); + try { if (addIngredients) { const result = await recipectl.addIngredientToRecipe(data, recipeID as string);
{each.name}
{each.quantity} {each.quantity == 1 ? each.unit : (each.unit + "s")} {each.name}
No ingredients for this recipe