From 99829533fd19dec8abc8a9055ceef47325e00244 Mon Sep 17 00:00:00 2001 From: Mikayla Dobson <93477693+innocuous-symmetry@users.noreply.github.com> Date: Thu, 16 Feb 2023 10:35:37 -0600 Subject: [PATCH] workshopping multi select --- client/src/components/Sandbox.tsx | 6 +- client/src/components/pages/AddRecipe.tsx | 120 +++++++++++++----- client/src/components/pages/Login.tsx | 2 +- .../components/pages/Register/aboutyou.tsx | 2 +- client/src/components/ui/Form.tsx | 27 ++-- client/src/components/ui/Selector.tsx | 30 ++--- client/src/context/SelectorContext.tsx | 31 +++++ client/src/context/SelectorProvider.tsx | 40 ++++++ client/src/main.tsx | 9 +- client/src/sass/App.scss | 4 + client/src/util/API.ts | 5 + client/src/util/types.ts | 8 ++ 12 files changed, 216 insertions(+), 68 deletions(-) create mode 100644 client/src/context/SelectorContext.tsx create mode 100644 client/src/context/SelectorProvider.tsx diff --git a/client/src/components/Sandbox.tsx b/client/src/components/Sandbox.tsx index fe314e2..53984f7 100644 --- a/client/src/components/Sandbox.tsx +++ b/client/src/components/Sandbox.tsx @@ -5,11 +5,7 @@ import { useAuthContext } from "../context/AuthContext"; import { ICollection, IIngredient } from "../schemas"; import { Button, Page } from "./ui"; import API from "../util/API"; - -interface OptionType { - value: number - label: string -} +import { OptionType } from "../util/types"; export default function Sandbox() { const [ingredients, setIngredients] = useState>([]); diff --git a/client/src/components/pages/AddRecipe.tsx b/client/src/components/pages/AddRecipe.tsx index dccdb1b..15a43ac 100644 --- a/client/src/components/pages/AddRecipe.tsx +++ b/client/src/components/pages/AddRecipe.tsx @@ -1,18 +1,105 @@ import { useCallback, useRef, useEffect, useState, createRef } from "react"; import { useAuthContext } from "../../context/AuthContext"; import { Button, Card, Divider, Form, Page, Panel } from "../ui" -import { IRecipe } from "../../schemas"; +import { IIngredient, IRecipe } from "../../schemas"; import API from "../../util/API"; +import Creatable from "react-select/creatable"; +import { OptionType } from "../../util/types"; +import { useSelectorContext } from "../../context/SelectorContext"; +import { MultiValue } from "react-select"; const AddRecipe = () => { const { user, token } = useAuthContext(); - const [input, setInput] = useState({ name: '', preptime: '', description: '', authoruserid: '', ingredients: [] }) - const [toast, setToast] = useState(<>) + const { + data, setData, selector, setSelector, + options, setOptions, selected, setSelected, + onChange, onCreateOption + } = useSelectorContext(); + const [triggerChange, setTriggerChange] = useState(false); + const [form, setForm] = useState(); + const [toast, setToast] = useState(<>) + const [input, setInput] = useState({ name: '', preptime: '', description: '', authoruserid: '' }) + + // clear out selector state on page load + useEffect(() => { + setData(new Array()); + setSelected(new Array()); + setOptions(new Array()); + }, []) + + // store all ingredients on page mount + useEffect(() => { + token && (async() => { + const ingredients = new API.Ingredient(token); + const result = await ingredients.getAll(); + + if (result) { + setData((prev) => [...prev, ...result]); + + // once async data is received, derive its new states + setOptions(result.map((each: IIngredient) => { + return { label: each.name, value: each.id } + })); + } + })(); + }, [token]) + + useEffect(() => { + console.log(selected); + }, [selected]) + + useEffect(() => { + if (data.length) { + console.log('caught'); + // create dropdown from new data + const selectorInstance = ) => onChange(selection)} + onCreateOption={(input: string) => onCreateOption(input, () => {})} + /> + data.length && setSelector(selectorInstance); + setTriggerChange(true); + } + }, [data, options]) + + // once the dropdown data has populated, mount it within the full form + useEffect(() => { + triggerChange && setForm( + _config={{ + parent: "AddRecipe", + keys: ["name", "preptime", "course", "cuisine", "ingredients", "description"], + labels: ["Recipe Name:", "Prep Time:", "Course:", "Cuisine:", "Ingredients:", "Description:"], + dataTypes: ['text', 'text', 'custom picker', 'custom picker', 'SELECTOR', 'TINYMCE'], + initialState: input, + getState: getFormState, + richTextInitialValue: "

Enter recipe details here!

", + selectorInstance: selector + }} /> + ) + }, [triggerChange]) + + // once user information is available, store it in recipe data + useEffect(() => { + if (!user) return; + user && setInput((prev: IRecipe) => { + return { + ...prev, + authoruserid: user.id! + } + }) + }, [user]) + + // store input data from form const getFormState = useCallback((data: IRecipe) => { setInput(data); }, [input]) + // submit handler const handleCreate = async () => { if (!token) return; @@ -23,8 +110,6 @@ const AddRecipe = () => { } } - console.log('good to go!') - const recipe = new API.Recipe(token); const result = await recipe.post(input); @@ -38,20 +123,6 @@ const AddRecipe = () => { ) } - - useEffect(() => { - if (!user) return; - user && setInput((prev: IRecipe) => { - return { - ...prev, - authoruserid: user.id! - } - }) - }, [user]) - - useEffect(() => { - console.log(input); - }, [input]) return ( @@ -59,16 +130,7 @@ const AddRecipe = () => { -
Enter recipe details here!

" - }} /> - + { form }
{ toast }
diff --git a/client/src/components/pages/Login.tsx b/client/src/components/pages/Login.tsx index 4ad816a..0cb4f6d 100644 --- a/client/src/components/pages/Login.tsx +++ b/client/src/components/pages/Login.tsx @@ -43,7 +43,7 @@ export default function Login() { - _config={{ parent: 'login', keys: Object.keys(input), labels: ["Email", "Password"], diff --git a/client/src/components/pages/Register/aboutyou.tsx b/client/src/components/pages/Register/aboutyou.tsx index b5893d4..6eeacfd 100644 --- a/client/src/components/pages/Register/aboutyou.tsx +++ b/client/src/components/pages/Register/aboutyou.tsx @@ -54,7 +54,7 @@ const AboutYou: RegisterVariantType = ({ transitionDisplay }) => { - _config={{ parent: "register", keys: ['firstname', 'lastname', 'handle', 'email', 'password'], initialState: input, diff --git a/client/src/components/ui/Form.tsx b/client/src/components/ui/Form.tsx index ad60945..aa25281 100644 --- a/client/src/components/ui/Form.tsx +++ b/client/src/components/ui/Form.tsx @@ -1,6 +1,8 @@ import { ChangeEvent, FC, useEffect, useState } from "react" import { v4 } from "uuid" import { useAuthContext } from "../../context/AuthContext"; +import { useSelectorContext } from "../../context/SelectorContext"; +import SelectorProvider from "../../context/SelectorProvider"; import { IIngredient, IUser } from "../../schemas"; import API from "../../util/API"; import RichText from "./RichText" @@ -16,26 +18,25 @@ export interface FormConfig { dataTypes?: string[] richTextInitialValue?: string extraStyles?: string + selectorInstance?: JSX.Element } interface FormProps { - parent: any _config: FormConfig } -const Form: FC = ({ parent, _config }) => { - type T = typeof parent; +function Form({ _config }: FormProps) { const { getState } = _config; const [config, setConfig] = useState>(); - const [state, setState] = useState(); + const [state, setState] = useState(_config.initialState); const [contents, setContents] = useState(); const { token } = useAuthContext(); // initial setup useEffect(() => { - if (!config) setConfig({ + setConfig({ ..._config, labels: _config.labels ?? _config.keys, dataTypes: _config.dataTypes ?? new Array(_config.keys?.length).fill("text"), @@ -101,14 +102,14 @@ const Form: FC = ({ parent, _config }) => { ) } else if (config.dataTypes![i] == 'SELECTOR') { - type StrongType = Partial & { id: number, name: string }; - const storedResult = await (async() => { - const result = await populateSelector(config?.labels![i] || ""); - if (result) return result as T[]; - return null; - })(); - - return config={config} idx={i} optionList={storedResult || []} /> + if (!config.selectorInstance) throw new Error("Dropdown was not provided to form component.") + + return ( +
+ + { config.selectorInstance } +
+ ) } else { return (
diff --git a/client/src/components/ui/Selector.tsx b/client/src/components/ui/Selector.tsx index bf64cc8..4f8ac1f 100644 --- a/client/src/components/ui/Selector.tsx +++ b/client/src/components/ui/Selector.tsx @@ -1,24 +1,22 @@ -import reactSelect from "react-select" -import makeAnimated from "react-select/animated"; import { FormConfig } from "./Form" -import { v4 } from "uuid" -interface Entity { - id: string | number - name?: string +interface OptionType { + value: number + label: string } -function Selector({ config, idx, optionList }: { config: FormConfig, idx: number, optionList: Array }) { -// const Selector: FC<{ optionList: Array }> = ({ optionList }) => { +interface SelectorProps { + config: FormConfig + idx: number + update: (e: any, idx: number) => void + optionList: Array + loader?: Array +} + +function Selector({ config, idx, update, optionList }: SelectorProps) { return ( -
- - -
+ <> + ) } diff --git a/client/src/context/SelectorContext.tsx b/client/src/context/SelectorContext.tsx new file mode 100644 index 0000000..8b4d80f --- /dev/null +++ b/client/src/context/SelectorContext.tsx @@ -0,0 +1,31 @@ +import { createContext, Dispatch, SetStateAction, useContext } from "react" +import { OptionType } from "../util/types" + +interface SelectorContextProps { + data: Array + setData: Dispatch>> | VoidFunction + selected: Array + setSelected: Dispatch>> | VoidFunction + options: Array + setOptions: Dispatch>> | VoidFunction + selector: JSX.Element + setSelector: Dispatch> | VoidFunction + onChange: (...params: any) => void + onCreateOption: (label: string, generateObject: (label: string, id: number) => T) => void +} + +const defaultValue: SelectorContextProps = { + data: new Array(), + setData: () => {}, + selected: new Array(), + setSelected: () => {}, + options: new Array(), + setOptions: () => {}, + selector: <>, + setSelector: () => {}, + onChange: () => {}, + onCreateOption: (label: string) => {}, +} + +export const SelectorContext = createContext>(defaultValue); +export const useSelectorContext = () => useContext(SelectorContext); \ No newline at end of file diff --git a/client/src/context/SelectorProvider.tsx b/client/src/context/SelectorProvider.tsx new file mode 100644 index 0000000..650d9f5 --- /dev/null +++ b/client/src/context/SelectorProvider.tsx @@ -0,0 +1,40 @@ +import { useState } from "react"; +import { OptionType } from "../util/types"; +import { SelectorContext } from "./SelectorContext"; + +function SelectorProvider({ children }: { children: JSX.Element | JSX.Element[] }) { + const [data, setData] = useState>([]); + const [selector, setSelector] = useState(<>) + const [options, setOptions] = useState>([]); + const [selected, setSelected] = useState>([]); + + /** + * Event handler for a change in selection state + */ + const onChange = (data: Array) => { + setSelected((prev) => [...prev, ...data]); + } + + const onCreateOption = (label: string, generateObject: (label: string, id: number) => T) => { + const newID = options.length + 1; + const newOption: OptionType = { label: label, value: newID } + + setOptions((prev) => [...prev, newOption]); + setSelected((prev) => [...prev, newOption]); + setData((prev) => [...prev, generateObject(label, newID)]); + } + + const providerValue = { + data, setData, selector, setSelector, + options, setOptions, selected, setSelected, + onChange, onCreateOption + } + + return ( + + { children } + + ) +} + +export default SelectorProvider; \ No newline at end of file diff --git a/client/src/main.tsx b/client/src/main.tsx index bec94b7..9f711e3 100644 --- a/client/src/main.tsx +++ b/client/src/main.tsx @@ -2,12 +2,15 @@ import React from 'react' import ReactDOM from 'react-dom/client' import App from './App' import AuthProvider from './context/AuthProvider' +import SelectorProvider from './context/SelectorProvider' import './sass/index.scss' ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( - - - + + + + + ) diff --git a/client/src/sass/App.scss b/client/src/sass/App.scss index d038364..8f4e923 100644 --- a/client/src/sass/App.scss +++ b/client/src/sass/App.scss @@ -7,3 +7,7 @@ text-align: center; background-color: $richblack; } + +.ui-creatable-component { + margin-bottom: 10rem; +} \ No newline at end of file diff --git a/client/src/util/API.ts b/client/src/util/API.ts index 9ef77c8..54f6a85 100644 --- a/client/src/util/API.ts +++ b/client/src/util/API.ts @@ -188,6 +188,11 @@ module API { constructor(token: string) { 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); + } } export class Collection extends RestController { diff --git a/client/src/util/types.ts b/client/src/util/types.ts index aa39b74..d36b754 100644 --- a/client/src/util/types.ts +++ b/client/src/util/types.ts @@ -38,6 +38,14 @@ interface CheckboxProps { FormElement: typeof Form } +/** + * Type declaration for react-select dropdown options + */ +export interface OptionType { + value: number + label: string +} + export type PageComponent = FC export type PanelComponent = FC export type ButtonComponent = FC