fraction support on addRecipe, working on state management

This commit is contained in:
Mikayla Dobson
2023-02-19 11:16:26 -06:00
parent 949762f3a0
commit 47360518ce
6 changed files with 289 additions and 101 deletions

View File

@@ -2,22 +2,17 @@ import { Autocomplete, TextField } from "@mui/material"
import { ChangeEvent, useEffect, useRef, useState } from "react";
import { useAuthContext } from "../../context/AuthContext";
import { DropdownData, IIngredient } from "../../schemas";
import { IngredientFieldData } from "../../util/types";
import { Button } from "../ui";
interface IngredientSelectorProps {
position: number
ingredients: IIngredient[]
units: DropdownData[]
getRowState: (input: IngredientFieldData) => void
destroy: (position: number) => void
}
interface RowState {
quantity: number
measurement: string | null
ingredientSelection: string | null
ingredients: IIngredient[]
}
const createIngredient = (name: string, userid: string | number) => {
return {
name: name,
@@ -25,34 +20,102 @@ const createIngredient = (name: string, userid: string | number) => {
} as IIngredient
}
function IngredientSelector({ position, ingredients, units, destroy }: IngredientSelectorProps) {
const quantityOptions: readonly any[] = [
{ name: "1/8" , value: 0.125 },
{ name: "1/4" , value: 0.250 },
{ name: "1/3" , value: 0.333 },
{ name: "3/8" , value: 0.375 },
{ name: "1/2" , value: 0.500 },
{ name: "5/8" , value: 0.625 },
{ name: "2/3" , value: 0.666 },
{ name: "3/4" , value: 0.750 },
{ name: "7/8" , value: 0.875 },
{ name: "1 1/4" , value: 1.250 },
{ name: "1 1/3" , value: 1.333 },
{ name: "1 1/2" , value: 1.500 },
{ name: "1 2/3" , value: 1.666 },
{ name: "1 3/4" , value: 1.750 },
{ name: "2 1/4" , value: 2.250 },
{ name: "2 1/3" , value: 2.333 },
{ name: "2 1/2" , value: 2.500 },
{ name: "2 3/4" , value: 2.750 },
{ name: "3 1/4" , value: 3.250 },
{ name: "3 1/2" , value: 3.500 },
{ name: "3 3/4" , value: 3.750 },
{ name: "4 1/4" , value: 4.250 },
{ name: "4 1/2" , value: 4.500 },
{ name: "4 3/4" , value: 4.750 },
{ name: "5 1/4" , value: 5.250 },
{ name: "5 1/2" , value: 5.500 },
{ name: "5 3/4" , value: 5.750 },
]
function IngredientSelector({ position, ingredients, units, getRowState, destroy }: IngredientSelectorProps) {
const { user } = useAuthContext();
const [ingredientOptions, setIngredientOptions] = useState(ingredients.map(each => each.name));
const [measurementUnits, setMeasurementUnits] = useState(units.map(each => each.name));
const [rowState, setRowState] = useState<RowState>({
quantity: 0,
measurement: null,
ingredientSelection: null,
const [rowState, setRowState] = useState<IngredientFieldData>({
quantity: undefined,
rowPosition: position,
measurement: undefined,
ingredientSelection: undefined,
ingredients: ingredients
})
const [quantityError, setQuantityError] = useState<null | string>(null);
useEffect(() => {
console.log("Row " + position + " state changed:");
console.log(rowState);
}, [rowState])
getRowState(rowState);
}, [rowState, setRowState])
function validateQuantity(input: any) {
const value = new Number(input).valueOf();
if (Number.isNaN(value)) {
console.log('is nan');
setQuantityError("Please provide a valid input (number)");
return;
}
if (!input) {
console.log('is null');
setRowState({ ...rowState, quantity: undefined });
setQuantityError(null);
return;
}
setQuantityError(null);
setRowState({ ...rowState, quantity: value });
}
return (
<table className="ingredient-widget"><tbody>
<tr>
<td className="quantity-of-unit">
<TextField variant="outlined" label="Quantity" onChange={(e) => setRowState({...rowState, quantity: parseFloat(e.target.value) })} />
<Autocomplete
freeSolo
autoHighlight
options={quantityOptions.map(each => each.name)}
className="ui-creatable-component"
onChange={(event, value) => {
console.log(value);
validateQuantity(quantityOptions.filter(option => (option.name) == value)[0]['value'] as number);
}}
renderInput={(params) => (
<TextField
{...params}
variant="outlined"
color={(rowState.quantity == null) ? (quantityError ? "error" : "info") : (quantityError ? "error" : "success")}
label={quantityError ?? "Quantity"}
onChange={(e) => validateQuantity(e.target.value)}
/>
)}
/>
</td>
<td className="ingredient-unit">
<Autocomplete
autoHighlight
options={measurementUnits}
options={units.map(each => each.name)}
className="ui-creatable-component"
renderInput={(params) => (
<TextField
@@ -62,7 +125,9 @@ function IngredientSelector({ position, ingredients, units, destroy }: Ingredien
/>
)}
onChange={(event, value) => {
setRowState({ ...rowState, measurement: value });
if (value) {
setRowState({ ...rowState, measurement: value });
}
}}
/>
</td>
@@ -103,7 +168,7 @@ function IngredientSelector({ position, ingredients, units, destroy }: Ingredien
return {
...prev,
ingredients: ingredients,
ingredientSelection: null
ingredientSelection: undefined
}
})
}

View File

@@ -2,15 +2,19 @@ import { useAuthContext } from "../../context/AuthContext";
import { useCallback, useEffect, useState } from "react";
import { Button, Card, Divider, Page, Panel } from "../ui"
import { DropdownData, IIngredient, IRecipe } from "../../schemas";
import { useSelectorContext } from "../../context/SelectorContext";
import IngredientSelector from "../derived/IngredientSelector";
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";
const AddRecipe = () => {
export default function AddRecipe() {
/**********************************
* STATE AND CONTEXT
*********************************/
const { user, token } = useAuthContext();
// received recipe data
@@ -21,76 +25,30 @@ const AddRecipe = () => {
const [measurements, setMeasurements] = useState<DropdownData[]>([]);
const [courseData, setCourseData] = useState<DropdownData[]>([]);
const [ingredientFields, setIngredientFields] = useState<Array<JSX.Element>>([]);
const [ingredientFieldData, setIngredientFieldData] = useState<Array<IngredientFieldData>>([]);
const [optionCount, setOptionCount] = useState(0);
// status reporting
const [toast, setToast] = useState(<></>)
// store all ingredients on page mount
useEffect(() => {
token && (async() => {
const ingredients = new API.Ingredient(token);
const _dropdowns = new API.Dropdowns(token);
const ingredientList = await ingredients.getAll();
const measurementList = await _dropdowns.getAllMeasurements();
const courseList = await _dropdowns.getAllCourses();
if (ingredientList) {
setIngredients((prev) => [...prev, ...ingredientList]);
}
if (measurementList) {
setMeasurements((prev) => [...prev, ...measurementList]);
}
if (courseList) {
setCourseData((prev) => [...prev, ...courseList]);
}
})();
}, [token])
useEffect(() => {
if (ingredients.length && measurements.length) {
setIngredientFields([<IngredientSelector key={v4()} position={optionCount} ingredients={ingredients} units={measurements} destroy={destroySelector} />]);
}
}, [ingredients, measurements])
// once user information is available, store it in recipe data
useEffect(() => {
user && setInput((prev: IRecipe) => {
return {
...prev,
authoruserid: user.id!
}
})
}, [user])
// submit handler
const handleCreate = async () => {
if (!token) return;
for (let field of Object.keys(input)) {
if (!input[field as keyof IRecipe]) {
return;
}
}
const recipe = new API.Recipe(token);
const result = await recipe.post(input);
const recipeID = result.recipe.id;
const recipeName = result.recipe.name;
setToast(
<Card>
<p>Created recipe {recipeName} successfully!</p>
<p>View your new recipe <a href={`/recipe/${recipeID}`}>here!</a></p>
</Card>
)
}
/**********************************
* CALLBACKS FOR CHILD COMPONENTS
*********************************/
// callback to retrieve state from ingredient rows
const getRowState = useCallback((value: IngredientFieldData) => {
setIngredientFieldData((prev) => {
const newState = prev;
newState[value.rowPosition] = value;
return newState;
});
}, [ingredientFieldData])
// callback passed to each ingredient row to enable the row to be closed
const destroySelector = useCallback((position: number) => {
setIngredientFieldData((prev) => {
return [...prev.filter(each => each.rowPosition !== position)];
})
setIngredientFields((prev) => {
const newState = new Array<JSX.Element>();
@@ -106,15 +64,118 @@ const AddRecipe = () => {
})
}, [ingredientFields]);
function handleNewOption() {
setIngredientFields((prev) => [...prev, <IngredientSelector position={optionCount + 1} key={v4()} ingredients={ingredients} units={measurements} destroy={destroySelector} />])
setOptionCount(prev => prev + 1);
}
/**********************************
* PAGE MOUNT BEHAVIOR AND SIDE EFFECTS
*********************************/
// store all ingredients on page mount
useEffect(() => {
token && (async() => {
const ingredients = new API.Ingredient(token);
const _dropdowns = new API.Dropdowns(token);
const ingredientList = await ingredients.getAll();
const measurementList = await _dropdowns.getAllMeasurements();
const courseList = await _dropdowns.getAllCourses();
function filterDuplicateEntries(receivedList: any[], previousState: any) {
let newEntries = [];
for (let each of receivedList) {
if (previousState.includes(each)) {
continue;
}
newEntries.push(each);
}
return newEntries;
}
if (ingredientList) {
setIngredients((prev) => {
let newEntries = filterDuplicateEntries(ingredientList, prev);
return [...prev, ...newEntries];
});
}
if (measurementList) {
setMeasurements((prev) => {
let newEntries = filterDuplicateEntries(measurementList, prev);
return [...prev, ...newEntries];
});
}
if (courseList) {
setCourseData((prev) => {
let newEntries = filterDuplicateEntries(courseList, prev);
return [...prev, ...newEntries];
});
}
})();
}, [token])
// mount the ingredient selection section once dependencies have loaded
useEffect(() => {
if (ingredients.length && measurements.length) {
setIngredientFields([<IngredientSelector key={v4()} position={optionCount} ingredients={ingredients} units={measurements} getRowState={getRowState} destroy={destroySelector} />]);
}
}, [ingredients, measurements])
useEffect(() => {
console.log(courseData);
}, [courseData])
console.log(ingredientFieldData);
}, [getRowState]);
/**********************************
* PAGE SPECIFIC FUNCTIONS
*********************************/
// submit handler
const handleCreate = async () => {
if (!user || !token) return;
// inject current user id into recipe entry
setInput({ ...input, authoruserid: user.id! });
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) {
continue;
}
if (!input[field as keyof IRecipe]) {
return;
}
}
const recipe = new API.Recipe(token);
const result = await recipe.post(input);
if (result) {
const recipeID = result.recipe.id;
const recipeName = result.recipe.name;
setToast(
<Toast>
<p>Created recipe {recipeName} successfully!</p>
<p>View your new recipe <a href={`/recipe/${recipeID}`}>here!</a></p>
</Toast>
)
} else {
setToast(
<Toast variant="fail">
<p>Error creating your recipe</p>
<p>Please refresh the browser window and try again.</p>
</Toast>
)
}
}
// logic for inserting a new ingredient row
function handleNewOption() {
setIngredientFields((prev) => [...prev, <IngredientSelector position={optionCount + 1} key={v4()} ingredients={ingredients} units={measurements} getRowState={getRowState} destroy={destroySelector} />])
setOptionCount(prev => prev + 1);
}
/**********************************
* RENDER
*********************************/
return (
<Protect redirect="/add-recipe">
<h1>Add a New Recipe</h1>
@@ -143,6 +204,7 @@ const AddRecipe = () => {
getOptionLabel={(option) => option.name}
/>}
</div>
<Button onClick={() => console.log(ingredientFieldData)}>Ingredient Field Data</Button>
{ ingredients && (
<>
@@ -170,5 +232,3 @@ const AddRecipe = () => {
</Protect>
)
}
export default AddRecipe;

View File

@@ -0,0 +1,19 @@
import { FC } from "react";
import "/src/sass/components/Toast.scss";
type StyleVariant = "success" | "fail" | "warning"
interface ToastProps {
children: JSX.Element | JSX.Element[]
variant?: StyleVariant
}
const Toast: FC<ToastProps> = ({ children, variant = "success" }) => {
return (
<div className={`ui-component-toast toast-variant-${variant}`}>
{ children }
</div>
)
}
export default Toast;

View File

@@ -0,0 +1,18 @@
.ui-component-toast {
position: absolute;
bottom: 1rem;
right: 1rem;
padding: 1rem;
.success {
background-color: green;
}
.fail {
background-color: red;
}
.warning {
background-color: yellow;
}
}

View File

@@ -1,7 +1,7 @@
import { ChangeEvent, ChangeEventHandler, Dispatch, FC, ReactNode, SetStateAction } from "react";
import { useNavigate } from "react-router-dom";
import { Form } from "../components/ui";
import { IUser } from "../schemas";
import { DropdownData, IIngredient, IUser } from "../schemas";
export interface PortalBase {
children?: ReactNode | ReactNode[]
@@ -46,6 +46,14 @@ interface CheckboxProps {
FormElement: typeof Form
}
export interface IngredientFieldData {
rowPosition: number
quantity: number | undefined
measurement: DropdownData['name'] | undefined
ingredientSelection: IIngredient['name'] | IIngredient | undefined
ingredients: Array<IIngredient>
}
/**
* Type declaration for react-select dropdown options
*/

View File

@@ -12,13 +12,21 @@ import { friendRouter } from "./friend";
import { cuisineRouter } from "./cuisine";
import { courseRouter } from "./course";
import { dropdownValueRouter } from "./dropdownValues";
import { IUser } from "../schemas";
import { User } from "../models/user";
dotenv.config();
export const routes = async (app: Express) => {
// unprotected routes
authRoute(app);
let REQUESTCOUNT = 0;
export const routes = async (app: Express) => {
// simple request counting middleware
app.use('/', (req, res, next) => {
REQUESTCOUNT++;
console.log("count: ", REQUESTCOUNT);
next();
})
// middleware to check for auth on cookies on each request in protected routes
app.use('/app', async (req, res, next) => {
// pull jwt from request headers
@@ -27,18 +35,28 @@ export const routes = async (app: Express) => {
if (!token) {
res.status(403).send("Unauthorized, did not receive token");
} else {
jwt.verify(token, process.env.SESSIONSECRET as string, (err, data) => {
jwt.verify(token, process.env.SESSIONSECRET as string, async (err, data: any) => {
if (err) {
res.status(403).send(err);
} else {
// @ts-ignore
req.user = data.user;
const userInstance = new User();
const foundUser = await userInstance.getOneByID(data.user.id);
if (foundUser) {
req.user = data.user as IUser;
} else {
res.status(403).send("Unauthorized, user not registered");
}
next();
}
})
}
})
// unprotected routes
authRoute(app);
// protected routes
userRoute(app);
friendRouter(app);