in progress: handle add ingredients to recipe

This commit is contained in:
Mikayla Dobson
2023-02-25 20:21:29 -06:00
parent 63d0049450
commit 8a939e6a81
14 changed files with 195 additions and 76 deletions

View File

@@ -2,8 +2,6 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom'; import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { useAuthContext } from './context/AuthContext'; import { useAuthContext } from './context/AuthContext';
import jwtDecode from 'jwt-decode';
import API from './util/API';
// pages, ui, components, styles // pages, ui, components, styles
import Subscriptions from './components/pages/Subscriptions/Subscriptions'; import Subscriptions from './components/pages/Subscriptions/Subscriptions';
@@ -19,11 +17,10 @@ import CollectionBrowser from './components/pages/CollectionBrowser';
import { Navbar } from './components/ui'; import { Navbar } from './components/ui';
import GroceryList from './components/pages/GroceryList'; import GroceryList from './components/pages/GroceryList';
import GroceryListCollection from './components/pages/GroceryListCollection'; import GroceryListCollection from './components/pages/GroceryListCollection';
import { TokenType } from './util/types';
import './sass/App.scss';
import handleToken from './util/handleToken'; import handleToken from './util/handleToken';
import AddFriends from './components/pages/AddFriends'; import AddFriends from './components/pages/AddFriends';
import Sandbox from './components/Sandbox'; import Sandbox from './components/Sandbox';
import './sass/App.scss';
function App() { function App() {
const { setUser, token, setToken } = useAuthContext(); const { setUser, token, setToken } = useAuthContext();

View File

@@ -1,7 +1,6 @@
import { useAuthContext } from "../../context/AuthContext";
import { FC, useEffect, useState } from "react"; import { FC, useEffect, useState } from "react";
import { v4 } from "uuid"; import { v4 } from "uuid";
import { useAuthContext } from "../../context/AuthContext";
import { getAllUsers, getFriendships, getPendingFriendRequests, getUserByID } from "../../util/apiUtils";
import API from "../../util/API"; import API from "../../util/API";
import UserCard from "../ui/UserCard"; import UserCard from "../ui/UserCard";
import { IUser, IFriendship } from "../../schemas"; import { IUser, IFriendship } from "../../schemas";
@@ -18,9 +17,9 @@ const Friends: FC<{ targetUser?: IUser }> = ({ targetUser }) => {
(async function() { (async function() {
try { try {
const Friends = new API.Friendship(token); 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); setFriends(result);
} }

View File

@@ -1,15 +1,18 @@
import { useAuthContext } from "../../context/AuthContext"; // library/framework
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { Button, Card, Divider, Page, Panel } from "../ui" import { Autocomplete, TextField } from "@mui/material";
import { DropdownData, IIngredient, IRecipe } from "../../schemas"; import { v4 } from "uuid";
import IngredientSelector from "../derived/IngredientSelector";
// 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 Protect from "../../util/Protect";
import API from "../../util/API"; import API from "../../util/API";
import { v4 } from "uuid";
import RichText from "../ui/RichText"; // ui/components
import { Autocomplete, TextField } from "@mui/material"; import { Button, Card, Divider, Panel, RichText, Toast } from "../ui"
import { IngredientFieldData } from "../../util/types"; import IngredientSelector from "../derived/IngredientSelector";
import Toast from "../ui/Toast";
export default function AddRecipe() { export default function AddRecipe() {
/********************************** /**********************************
@@ -103,7 +106,7 @@ export default function AddRecipe() {
}); });
} }
if (courseList) { if (courseList && !courseData.length) {
setCourseData((prev) => { setCourseData((prev) => {
let newEntries = filterDuplicateEntries(courseList, prev); let newEntries = filterDuplicateEntries(courseList, prev);
return [...prev, ...newEntries]; return [...prev, ...newEntries];
@@ -112,22 +115,23 @@ export default function AddRecipe() {
})(); })();
}, [token]) }, [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(() => { useEffect(() => {
if (ingredients.length && measurements.length) { const conditionsMet = (ingredients.length && measurements.length) && (!ingredientFields.length);
if (conditionsMet) {
setIngredientFields([<IngredientSelector key={v4()} position={optionCount} ingredients={ingredients} units={measurements} getRowState={getRowState} destroy={destroySelector} />]); setIngredientFields([<IngredientSelector key={v4()} position={optionCount} ingredients={ingredients} units={measurements} getRowState={getRowState} destroy={destroySelector} />]);
} }
}, [ingredients, measurements]) }, [ingredients, measurements])
useEffect(() => {
console.log(ingredientFieldData);
}, [getRowState]);
/********************************** /**********************************
* PAGE SPECIFIC FUNCTIONS * PAGE SPECIFIC FUNCTIONS
*********************************/ *********************************/
// submit handler // submit handler
const handleCreate = async () => { async function handleCreate() {
if (!user || !token) return; if (!user || !token) return;
// initialize API handlers // initialize API handlers
@@ -153,31 +157,52 @@ export default function AddRecipe() {
} }
} }
let ingredientSelections = new Array<any>(); let preparedIngredientData = new Array<RecipeIngredient>();
let newIngredientCount = 0; let newIngredientCount = 0;
// handle ingredient row data // handle ingredient row data
for (let row of ingredientFieldData) { for (let row of ingredientFieldData) {
if (!row) continue;
console.log(row);
if (row.ingredientSelection === undefined) { if (row.ingredientSelection === undefined) {
messages.push("Please ensure you have included ingredient selections for all rows."); 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) { for (let ing of row.ingredients) {
// filter out recipes that already exist // filter out recipes that already exist
if (ingredients.filter(x => x.name == ing.name).includes(ing)) { if (!ingredients.filter(x => x.name == ing.name).includes(ing)) {
continue; // 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 // update the ingredient list
setIngredients((prev) => [...prev, ing]); 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; const recipeName = result.recipe.name;
let recipeIngredientCount = 0; let recipeIngredientCount = 0;
for (let ing of ingredientSelections) { for (let ing of preparedIngredientData) {
const ok = await recipeAPI.addIngredientToRecipe(ing, recipeID); const ok = await recipeAPI.addIngredientToRecipe(ing, recipeID);
if (ok) recipeIngredientCount++; if (ok) recipeIngredientCount++;
} }

View File

@@ -1,12 +1,13 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { Divider, Page, Panel } from "../ui"; 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 { getRecipeByID } from "../../util/apiUtils";
import Protect from "../../util/Protect"; import Protect from "../../util/Protect";
import API from "../../util/API"; import API from "../../util/API";
import { useAuthContext } from "../../context/AuthContext"; import { useAuthContext } from "../../context/AuthContext";
import ResourceNotFound from "./StatusPages/404"; import ResourceNotFound from "./StatusPages/404";
import { v4 } from "uuid";
export default function Recipe() { export default function Recipe() {
const { user, token } = useAuthContext(); const { user, token } = useAuthContext();
@@ -14,7 +15,7 @@ export default function Recipe() {
const [recipe, setRecipe] = useState<IRecipe | "no recipe">(); const [recipe, setRecipe] = useState<IRecipe | "no recipe">();
const [userData, setUserData] = useState<IUser>(); const [userData, setUserData] = useState<IUser>();
const [ingredientData, setIngredientData] = useState<IIngredient[]>([]); const [ingredientData, setIngredientData] = useState<RecipeIngredient[]>([]);
const [view, setView] = useState<JSX.Element>(<h1>Loading...</h1>); const [view, setView] = useState<JSX.Element>(<h1>Loading...</h1>);
useEffect(() => { useEffect(() => {
@@ -32,17 +33,19 @@ export default function Recipe() {
}, [token]) }, [token])
useEffect(() => { useEffect(() => {
if (recipe) { if (recipe && id) {
if (recipe === "no recipe") { if (recipe === "no recipe") {
setView(<ResourceNotFound><h2>We couldn't find a recipe with the ID {id}.</h2></ResourceNotFound>); setView(<ResourceNotFound><h2>We couldn't find a recipe with the ID {id}.</h2></ResourceNotFound>);
} else { } else {
if (!user || !token) return; 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 ingredientAPI = new API.Ingredient(token);
const result = await ingredientAPI.getAllForRecipe(id!); const result = await ingredientAPI.getAllForRecipe(id);
if (result.length) setIngredientData(result); if (result.length) setIngredientData(result);
}) })();
const selfAuthored = (recipe.authoruserid == user.id!); const selfAuthored = (recipe.authoruserid == user.id!);
if (selfAuthored) { if (selfAuthored) {
@@ -61,7 +64,7 @@ export default function Recipe() {
useEffect(() => { useEffect(() => {
if (!userData) return; if (!userData) return;
if (recipe && recipe !== "no recipe") { if (recipe && ingredientData && recipe !== "no recipe") {
setView( setView(
<Protect redirect={`/recipe/${id}`}> <Protect redirect={`/recipe/${id}`}>
<h1>{recipe.name}</h1> <h1>{recipe.name}</h1>
@@ -75,8 +78,11 @@ export default function Recipe() {
<h2>Ingredients:</h2> <h2>Ingredients:</h2>
{ ingredientData.length { ingredientData.length
? ingredientData.map((each: IIngredient) => <p>{each.name}</p>) ? ingredientData.map((each: RecipeIngredient) => (
: "No ingredients for this recipe" <div key={v4()}>
<p>{each.quantity} {each.quantity == 1 ? each.unit : (each.unit + "s")} {each.name}</p>
</div>
)) : <p>No ingredients for this recipe</p>
} }
<Divider /> <Divider />
@@ -86,7 +92,7 @@ export default function Recipe() {
</Protect> </Protect>
); );
} }
}, [userData, ingredientData]); }, [userData, recipe, ingredientData]);
return view return view
} }

View File

@@ -1,4 +1,5 @@
import { FC, useEffect, useState } from "react"; import { FC, useEffect, useState } from "react";
import { useAuthContext } from "../../context/AuthContext";
import Protect from "../../util/Protect"; import Protect from "../../util/Protect";
import Form from "./Form/Form"; import Form from "./Form/Form";
@@ -9,6 +10,8 @@ interface BrowserProps {
} }
const Browser: FC<BrowserProps> = ({ children, header, searchFunction }) => { const Browser: FC<BrowserProps> = ({ children, header, searchFunction }) => {
const { user, token } = useAuthContext();
const [form, setForm] = useState<any>(); const [form, setForm] = useState<any>();
useEffect(() => { useEffect(() => {

View File

@@ -6,10 +6,12 @@ import Form from "./Form/Form";
import Navbar from "./Navbar"; import Navbar from "./Navbar";
import Page from "./Page"; import Page from "./Page";
import Panel from "./Panel"; import Panel from "./Panel";
import RichText from "./RichText";
import TextField from "./TextField"; import TextField from "./TextField";
import Toast from "./Toast";
import Tooltip from "./Tooltip"; import Tooltip from "./Tooltip";
import UserCard from "./UserCard"; import UserCard from "./UserCard";
export { 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
} }

View File

@@ -56,29 +56,66 @@ module API {
} }
async getAll() { async getAll() {
const response = await this.instance.get(this.endpoint, this.headers); const response = await this.instance.get(this.endpoint, this.headers)
return Promise.resolve(response.data); .catch((err: AxiosError) => {
console.log(err.message);
return null;
});
return Promise.resolve(response?.data);
} }
async getByID(id: string) { async getByID(id: string) {
const response = await this.instance.get(this.endpoint + "/" + id, this.headers); const response = await this.instance.get(this.endpoint + "/" + id, this.headers)
return Promise.resolve(response.data); .catch((err: AxiosError) => {
console.log(err.message);
return null;
});
return Promise.resolve(response?.data);
} }
async post(data: T) { async post(data: T) {
console.log(data); try {
const response = await this.instance.post(this.endpoint, data, this.headers); const response = await this.instance.post(this.endpoint, JSON.stringify(data), this.headers);
return Promise.resolve(response.data); 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<T>) { async put(id: string, data: T | Partial<T>) {
const response = await this.instance.put(this.endpoint + "/" + id, JSON.stringify(data), this.headers); try {
return Promise.resolve(response.data); 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) { async delete(id: string) {
const response = await this.instance.delete(this.endpoint + '/' + id, this.headers); try {
return Promise.resolve(response.data); 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); 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) { async getTargetUserFriendships(id: string | number) {
try { try {
const response = await this.instance.get(this.endpoint + `?targetUser=${id}`, this.headers); 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) { async addIngredientToRecipe(ingredient: RecipeIngredient, recipeid: string | number) {
const response = await this.instance.post(this.endpoint + `?addIngredients=true&recipeID=${recipeid}`, JSON.stringify(ingredient), this.headers); const response = await this.instance.post(this.endpoint + `?addIngredients=true&recipeID=${recipeid}`, JSON.stringify(ingredient), this.headers)
return Promise.resolve(response.data); .catch((err) => {
console.log(err);
return null;
});
return Promise.resolve(response?.data);
} }
} }

View File

@@ -14,7 +14,9 @@ instance.interceptors.response.use((res: AxiosResponse<any,any>) => {
return res; return res;
}, (err) => { }, (err) => {
return Promise.reject(err); console.log(err);
// return err;
// return Promise.reject(err);
}) })
export default instance; export default instance;

2
dev.sh Normal file
View File

@@ -0,0 +1,2 @@
#! /bin/bash
concurrently "cd server && npm run dev" "cd client && npm run dev"

24
package.json Normal file
View File

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

View File

@@ -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) { async getOne(id: string) {
try { try {
const result = await IngredientInstance.getOne(id); const result = await IngredientInstance.getOne(id);

View File

@@ -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) { async post(data: IIngredient) {
try { try {
const statement = ` const statement = `

View File

@@ -10,9 +10,16 @@ export const ingredientRoute = (app: Express) => {
app.use('/app/ingredient', router); app.use('/app/ingredient', router);
router.get('/', async (req, res, next) => { router.get('/', async (req, res, next) => {
const { recipeID } = req.query;
try { try {
const result: CtlResponse<IIngredient[] | string> = await IngredientInstance.getAll(); if (recipeID) {
res.status(result.code).send(result.data); const result = await IngredientInstance.getAllForRecipe(recipeID as string);
res.status(result.code).send(result.data);
} else {
const result: CtlResponse<IIngredient[] | string> = await IngredientInstance.getAll();
res.status(result.code).send(result.data);
}
} catch(e) { } catch(e) {
next(e); next(e);
} }

View File

@@ -59,6 +59,8 @@ export const recipeRoute = (app: Express) => {
const data = req.body; const data = req.body;
const { addIngredients, recipeID } = req.query; const { addIngredients, recipeID } = req.query;
console.log(data);
try { try {
if (addIngredients) { if (addIngredients) {
const result = await recipectl.addIngredientToRecipe(data, recipeID as string); const result = await recipectl.addIngredientToRecipe(data, recipeID as string);