extensible inputs on recipe add page

This commit is contained in:
Mikayla Dobson
2023-02-16 16:50:18 -06:00
parent 8f1cfa0ad9
commit 46454a84c2
21 changed files with 218 additions and 77 deletions

View File

@@ -51,7 +51,7 @@ const Friends: FC<{ targetUser?: IUser }> = ({ targetUser }) => {
<> <>
{ userList.length ? { userList.length ?
( (
<Card extraStyles="flex-row"> <Card extraClasses="flex-row">
<h2>Friends ({ userList?.length ?? "0" }):</h2> <h2>Friends ({ userList?.length ?? "0" }):</h2>
<div className="friends-list"> <div className="friends-list">

View File

@@ -0,0 +1,47 @@
import { Autocomplete, TextField } from "@mui/material"
import { useRef, useState } from "react";
import { IIngredient } from "../../schemas";
import { Button } from "../ui";
interface IngredientSelectorProps {
position: number
ingredients: IIngredient[]
destroy: (position: number) => void
}
function IngredientSelector({ position, ingredients, destroy }: IngredientSelectorProps) {
const [options, setOptions] = useState(ingredients.map(each => each.name));
const [newOptions, setNewOptions] = useState(new Array<string>());
const [selected, setSelected] = useState(new Array<string>());
return (
<div style={{ display: "flex", flexDirection: "row" }}>
<Autocomplete
autoHighlight
options={options}
className="ui-creatable-component"
renderInput={(params) => (
<TextField
{...params}
variant="filled"
placeholder="Ingredient Name"
/>
)}
onKeyDown={(e) => {
if (e.code == 'Enter') {
const inputVal: string = e.target['value' as keyof EventTarget].toString();
console.log(inputVal)
if (inputVal.length) {
setSelected(prev => [...prev, inputVal])
setOptions((prev) => [...prev, inputVal]);
}
}
}}
/>
{/* @ts-ignore */}
<Button onClick={() => destroy(position)}>Close</Button>
</div>
)
}
export default IngredientSelector

View File

@@ -1,34 +1,29 @@
import { useCallback, useRef, useEffect, useState, createRef } from "react";
import { useAuthContext } from "../../context/AuthContext"; import { useAuthContext } from "../../context/AuthContext";
import { LegacyRef, MutableRefObject, useCallback, useEffect, useRef, useState } from "react";
import { Button, Card, Divider, Form, Page, Panel } from "../ui" import { Button, Card, Divider, Form, Page, Panel } from "../ui"
import { IIngredient, IRecipe } from "../../schemas"; import { IIngredient, IRecipe } from "../../schemas";
import API from "../../util/API"; import API from "../../util/API";
import Creatable from "react-select/creatable";
import { OptionType } from "../../util/types";
import { createOptionFromText, useSelectorContext } from "../../context/SelectorContext"; import { createOptionFromText, useSelectorContext } from "../../context/SelectorContext";
import { MultiValue } from "react-select"; import IngredientSelector from "../derived/IngredientSelector";
import { Autocomplete, Chip, TextField } from "@mui/material"; import { v4 } from "uuid";
const AddRecipe = () => { const AddRecipe = () => {
const { user, token } = useAuthContext(); const { user, token } = useAuthContext();
const { const { data, setData, options, setOptions } = useSelectorContext();
data, setData, selector, setSelector, const [ingredientFields, setIngredientFields] = useState<Array<JSX.Element>>([]);
options, setOptions, selected, setSelected,
onChange, onCreateOption
} = useSelectorContext();
const [triggerChange, setTriggerChange] = useState(false); const [triggerChange, setTriggerChange] = useState(false);
const [optionCount, setOptionCount] = useState(0); const [optionCount, setOptionCount] = useState(0);
const [form, setForm] = useState<JSX.Element>();
const [toast, setToast] = useState(<></>) const [toast, setToast] = useState(<></>)
const [input, setInput] = useState<IRecipe>({ name: '', preptime: '', description: '', authoruserid: '' }) const [input, setInput] = useState<IRecipe>({ name: '', preptime: '', description: '', authoruserid: '' })
const initialIngredient = useRef(null);
// clear out selector state on page load // clear out selector state on page load
useEffect(() => { /* useEffect(() => {
setData(new Array<IIngredient>()); setData(new Array<IIngredient>());
setSelected(new Array<string>()); setSelected(new Array<string>());
setOptions(new Array<OptionType>()); setOptions(new Array<OptionType>());
}, []) }, []) */
// store all ingredients on page mount // store all ingredients on page mount
useEffect(() => { useEffect(() => {
@@ -44,18 +39,14 @@ const AddRecipe = () => {
return { label: each.name, value: each.id } return { label: each.name, value: each.id }
})); }));
setOptionCount(result.length); setIngredientFields([<IngredientSelector key={v4()} position={optionCount} ingredients={result} destroy={destroySelector} />]);
} }
})(); })();
}, [token]) }, [token])
useEffect(() => {
console.log(selected);
}, [selected])
useEffect(() => { useEffect(() => {
if (data.length) { if (data.length) {
const autocompleteInstance = ( /* const autocompleteInstance = (
<Autocomplete <Autocomplete
multiple multiple
freeSolo freeSolo
@@ -87,7 +78,7 @@ const AddRecipe = () => {
/> />
)} )}
/> />
) ) */
// create dropdown from new data // create dropdown from new data
/* /*
@@ -102,13 +93,13 @@ const AddRecipe = () => {
/> />
*/ */
data.length && setSelector(autocompleteInstance); // data.length && setSelector(autocompleteInstance);
setTriggerChange(true); setTriggerChange(true);
} }
}, [data, options, selected]) }, [data, options])
// once the dropdown data has populated, mount it within the full form // once the dropdown data has populated, mount it within the full form
useEffect(() => { /* useEffect(() => {
triggerChange && setForm( triggerChange && setForm(
<Form<IRecipe> _config={{ <Form<IRecipe> _config={{
parent: "AddRecipe", parent: "AddRecipe",
@@ -118,10 +109,9 @@ const AddRecipe = () => {
initialState: input, initialState: input,
getState: getFormState, getState: getFormState,
richTextInitialValue: "<p>Enter recipe details here!</p>", richTextInitialValue: "<p>Enter recipe details here!</p>",
selectorInstance: selector
}} /> }} />
) )
}, [triggerChange]) }, [triggerChange]) */
useEffect(() => { useEffect(() => {
console.log(options); console.log(options);
@@ -139,6 +129,7 @@ const AddRecipe = () => {
}, [user]) }, [user])
// store input data from form // store input data from form
/*
const getFormState = useCallback((data: IRecipe) => { const getFormState = useCallback((data: IRecipe) => {
setInput(data); setInput(data);
}, [input]) }, [input])
@@ -153,7 +144,11 @@ const AddRecipe = () => {
setSelected((prev) => { setSelected((prev) => {
return prev.filter(option => option !== target); return prev.filter(option => option !== target);
}) })
} } */
useEffect(() => {
return;
}, [ingredientFields])
// submit handler // submit handler
const handleCreate = async () => { const handleCreate = async () => {
@@ -179,14 +174,70 @@ const AddRecipe = () => {
</Card> </Card>
) )
} }
const destroySelector = useCallback((position: number) => {
setIngredientFields((prev) => {
const newState = new Array<JSX.Element>();
for (let i = 0; i < prev.length; i++) {
if (i === position) {
continue;
} else {
newState.push(prev[i]);
}
}
return newState;
})
}, [ingredientFields]);
function handleNewOption() {
setIngredientFields((prev) => [...prev, <IngredientSelector position={optionCount + 1} key={v4()} ingredients={data} destroy={destroySelector} />])
setOptionCount(prev => prev + 1);
}
useEffect(() => {
console.log(optionCount);
}, [optionCount])
return ( return (
<Page> <Page>
<h1>Add a New Recipe</h1> <h1>Add a New Recipe</h1>
<Divider /> <Divider />
<Panel extraStyles="width-80"> <Panel id="create-recipe-panel" extraClasses="ui-form-component width-80">
{ form } <div className="form-row">
<label>Recipe Name:</label>
<input />
</div>
<div className="form-row">
<label>Prep Time:</label>
<input />
</div>
<div className="form-row">
<label>Course:</label>
<input />
</div>
{ data && (
<Card extraClasses="form-row flex-row ingredient-card">
<label>Ingredients:</label>
<div id="ingredient-container">
{ ingredientFields }
<Button onClick={handleNewOption}>Add Ingredient</Button>
</div>
</Card>
)}
<Divider />
<div className="form-row">
<label>Description:</label>
{ "description here" }
</div>
<Button onClick={handleCreate}>Create Recipe!</Button> <Button onClick={handleCreate}>Create Recipe!</Button>
<div id="toast">{ toast }</div> <div id="toast">{ toast }</div>

View File

@@ -3,7 +3,7 @@ import { AuthContext, useAuthContext } from "../../context/AuthContext";
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
import { IUser, IUserAuth } from "../../schemas"; import { IUser, IUserAuth } from "../../schemas";
import { Button, Form, Page, Panel } from "../ui"; import { Button, Form, Page, Panel } from "../ui";
import { FormConfig } from "../ui/Form"; import { FormConfig } from "../ui/Form/Form";
import API from "../../util/API"; import API from "../../util/API";
export default function Login() { export default function Login() {
@@ -41,7 +41,7 @@ export default function Login() {
<Page> <Page>
<h1>Hello! Nice to see you again.</h1> <h1>Hello! Nice to see you again.</h1>
<Panel extraStyles="form-panel"> <Panel extraClasses="form-panel">
<Form<IUserAuth> _config={{ <Form<IUserAuth> _config={{
parent: 'login', parent: 'login',

View File

@@ -6,7 +6,7 @@ import { IUser } from "../../../schemas";
import API from "../../../util/API"; import API from "../../../util/API";
import { Button, Page, Panel } from "../../ui"; import { Button, Page, Panel } from "../../ui";
import Divider from "../../ui/Divider"; import Divider from "../../ui/Divider";
import Form from "../../ui/Form"; import Form from "../../ui/Form/Form";
const blankUser: IUser = { const blankUser: IUser = {
firstname: '', firstname: '',
@@ -52,7 +52,7 @@ const AboutYou: RegisterVariantType = ({ transitionDisplay }) => {
<h2>Tell us a bit about yourself:</h2> <h2>Tell us a bit about yourself:</h2>
<Panel extraStyles="form-panel two-columns"> <Panel extraClasses="form-panel two-columns">
<Form<IUser> _config={{ <Form<IUser> _config={{
parent: "register", parent: "register",

View File

@@ -8,7 +8,7 @@ const Welcome = () => {
const { user } = useAuthContext(); const { user } = useAuthContext();
const authUserActions = ( const authUserActions = (
<Panel extraStyles="inherit-background c-papyrus uppercase flexrow"> <Panel extraClasses="inherit-background c-papyrus uppercase flexrow">
<Button onClick={() => navigate('/explore')}>Browse Recipes</Button> <Button onClick={() => navigate('/explore')}>Browse Recipes</Button>
<Button onClick={() => navigate('/subscriptions')}>Subscriptions</Button> <Button onClick={() => navigate('/subscriptions')}>Subscriptions</Button>
<Button onClick={() => navigate('/grocery-list')}>Grocery Lists</Button> <Button onClick={() => navigate('/grocery-list')}>Grocery Lists</Button>
@@ -16,27 +16,27 @@ const Welcome = () => {
) )
const callToRegister = ( const callToRegister = (
<Panel extraStyles="inherit-background c-papyrus uppercase"> <Panel extraClasses="inherit-background c-papyrus uppercase">
<h2>Ready to get started?</h2> <h2>Ready to get started?</h2>
<Button onClick={() => navigate('/register')}>Register</Button> <Button onClick={() => navigate('/register')}>Register</Button>
</Panel> </Panel>
) )
return ( return (
<Page extraStyles="narrow-dividers"> <Page extraClasses="narrow-dividers">
<Panel extraStyles='inherit-background c-papyrus uppercase'> <Panel extraClasses='inherit-background c-papyrus uppercase'>
<h1>Welcome to Recipin</h1> <h1>Welcome to Recipin</h1>
</Panel> </Panel>
<Divider /> <Divider />
<Panel extraStyles="inherit-background c-papyrus uppercase"> <Panel extraClasses="inherit-background c-papyrus uppercase">
<h2>Simple Recipe Management and Sharing for the Home</h2> <h2>Simple Recipe Management and Sharing for the Home</h2>
</Panel> </Panel>
<Divider /> <Divider />
<Panel extraStyles="inherit-background c-papyrus uppercase"> <Panel extraClasses="inherit-background c-papyrus uppercase">
<h2>Build Shopping Lists Directly from Your Recipes</h2> <h2>Build Shopping Lists Directly from Your Recipes</h2>
</Panel> </Panel>

View File

@@ -1,6 +1,6 @@
import { FC, useEffect, useState } from "react"; import { FC, useEffect, useState } from "react";
import Protect from "../../util/Protect"; import Protect from "../../util/Protect";
import Form from "./Form"; import Form from "./Form/Form";
interface BrowserProps { interface BrowserProps {
children?: JSX.Element[] children?: JSX.Element[]

View File

@@ -1,9 +1,9 @@
import { ButtonComponent } from "../../util/types" import { ButtonComponent } from "../../util/types"
import "/src/sass/components/Button.scss"; import "/src/sass/components/Button.scss";
const Button: ButtonComponent = ({ onClick = (() => {}), children, extraStyles, disabled = false, disabledText = null }) => { const Button: ButtonComponent = ({ onClick = (() => {}), children, extraClasses, disabled = false, disabledText = null }) => {
return ( return (
<button onClick={onClick} disabled={disabled} className={`ui-button ${extraStyles || ''}`}> <button onClick={onClick} disabled={disabled} className={`ui-button ${extraClasses || ''}`}>
{ disabled ? (disabledText || children || "Button") : (children || "Button") } { disabled ? (disabledText || children || "Button") : (children || "Button") }
</button> </button>
) )

View File

@@ -1,9 +1,9 @@
import { FC } from "react"; import { FC } from "react";
const Card: FC<{ children?: JSX.Element | JSX.Element[], extraStyles?: string }> = ({ children = <></>, extraStyles = ""}) => { const Card: FC<{ children?: JSX.Element | JSX.Element[], extraClasses?: string }> = ({ children = <></>, extraClasses = ""}) => {
return ( return (
<div className={`ui-card ${extraStyles}`}> <div className={`ui-card ${extraClasses}`}>
{ Array.isArray(children) ? <>{children}</> : children } { Array.isArray(children) ? <>{children}</> : children }
</div> </div>
) )

View File

@@ -4,9 +4,9 @@ import { PortalBase } from "../../util/types";
import "/src/sass/components/Dropdown.scss"; import "/src/sass/components/Dropdown.scss";
// expects to receive buttons as children // expects to receive buttons as children
const Dropdown: FC<PortalBase> = ({ children, extraStyles = null }) => { const Dropdown: FC<PortalBase> = ({ children, extraClasses = null }) => {
return ( return (
<Panel extraStyles={`ui-dropdown ${extraStyles}`}> <Panel extraClasses={`ui-dropdown ${extraClasses}`}>
{ children } { children }
</Panel> </Panel>
) )

View File

@@ -1,12 +1,12 @@
import { ChangeEvent, FC, useEffect, useState } from "react" import { ChangeEvent, FC, useEffect, useState } from "react"
import { v4 } from "uuid" import { v4 } from "uuid"
import { useAuthContext } from "../../context/AuthContext"; import { useAuthContext } from "../../../context/AuthContext";
import { useSelectorContext } from "../../context/SelectorContext"; import { useSelectorContext } from "../../../context/SelectorContext";
import SelectorProvider from "../../context/SelectorProvider"; import SelectorProvider from "../../../context/SelectorProvider";
import { IIngredient, IUser } from "../../schemas"; import { IIngredient, IUser } from "../../../schemas";
import API from "../../util/API"; import API from "../../../util/API";
import RichText from "./RichText" import RichText from "../RichText"
import Selector from "./Selector"; import Selector from "../Selector";
import "/src/sass/components/Form.scss"; import "/src/sass/components/Form.scss";
export interface FormConfig<T> { export interface FormConfig<T> {
@@ -17,7 +17,7 @@ export interface FormConfig<T> {
labels?: string[] labels?: string[]
dataTypes?: string[] dataTypes?: string[]
richTextInitialValue?: string richTextInitialValue?: string
extraStyles?: string extraClasses?: string
selectorInstance?: JSX.Element selectorInstance?: JSX.Element
} }
@@ -132,7 +132,7 @@ function Form<T>({ _config }: FormProps) {
}, [config]); }, [config]);
return ( return (
<div className={`ui-form-component ${_config.extraStyles ?? ""}`}> <div className={`ui-form-component ${_config.extraClasses ?? ""}`}>
{ contents } { contents }
</div> </div>
) )

View File

@@ -0,0 +1,38 @@
import { useEffect, useState } from "react";
import { v4 } from "uuid";
interface FormRowConfig {
parent: any
labelText: string
idx?: number
dataType?: string
}
function FormRow({ parent, labelText, idx, dataType = "string" }) {
const [row, setRow] = useState<JSX.Element>();
useEffect(() => {
switch (dataType) {
case "TINYMCE":
break;
case "string":
default:
setRow(
<div className="form-row" id={`${parent}-row-${idx || v4()}`} key={v4()}>
<label htmlFor={`${parent}-${each}`}>{labelText}</label>
<input
type={dataType}
id={`${parent}-${each}`}
onChange={(e) => update(e, idx)}
value={state[i as keyof T] as string}>
</input>
</div>
)
break;
}
}, [])
return (
<></>
)
}

View File

@@ -53,7 +53,7 @@ const LoggedIn = () => {
</div> </div>
{ {
dropdownActive && ( dropdownActive && (
<Dropdown extraStyles="top-menu-bar actions-bar"> <Dropdown extraClasses="top-menu-bar actions-bar">
<Button onClick={() => handleOptionSelect('/add-recipe')}>Add a Recipe</Button> <Button onClick={() => handleOptionSelect('/add-recipe')}>Add a Recipe</Button>
<Button onClick={() => handleOptionSelect("/add-friends")}>Add Friends</Button> <Button onClick={() => handleOptionSelect("/add-friends")}>Add Friends</Button>
<Button onClick={() => handleOptionSelect('/collections')}>My Collections</Button> <Button onClick={() => handleOptionSelect('/collections')}>My Collections</Button>
@@ -65,7 +65,7 @@ const LoggedIn = () => {
} }
{ {
searchActive && ( searchActive && (
<Dropdown extraStyles="top-menu-bar search-bar"> <Dropdown extraClasses="top-menu-bar search-bar">
<Button>Run Search</Button> <Button>Run Search</Button>
</Dropdown> </Dropdown>
) )

View File

@@ -5,10 +5,10 @@ import { PageComponent } from "../../util/types"
import Navbar from "./Navbar"; import Navbar from "./Navbar";
import "/src/sass/components/Page.scss"; import "/src/sass/components/Page.scss";
const Page: PageComponent = ({ extraStyles, children }) => { const Page: PageComponent = ({ extraClasses, children }) => {
return ( return (
<main id="view"> <main id="view">
<section className={`Page ${extraStyles || null}`}> <section className={`Page ${extraClasses || null}`}>
{ children || null } { children || null }
</section> </section>
</main> </main>

View File

@@ -1,12 +1,20 @@
import { PanelComponent } from "../../util/types"; import { PanelComponent } from "../../util/types";
import "/src/sass/components/Panel.scss"; import "/src/sass/components/Panel.scss";
const Panel: PanelComponent = ({ children, extraStyles }) => { const Panel: PanelComponent = ({ children, extraClasses, id }) => {
return ( if (id) {
<div className={`Panel ${extraStyles || ''}`}> return (
{ children || null } <div id={id} className={`Panel ${extraClasses || ''}`}>
</div> { children || null }
) </div>
)
} else {
return (
<div className={`Panel ${extraClasses || ''}`}>
{ children || null }
</div>
)
}
} }
export default Panel; export default Panel;

View File

@@ -1,4 +1,4 @@
import { FormConfig } from "./Form" import { FormConfig } from "./Form/Form"
interface OptionType { interface OptionType {
value: number value: number

View File

@@ -1,9 +1,9 @@
import { FC } from "react"; import { FC } from "react";
import { PortalBase } from "../../util/types"; import { PortalBase } from "../../util/types";
const Tooltip: FC<PortalBase> = ({ children, extraStyles = null }) => { const Tooltip: FC<PortalBase> = ({ children, extraClasses = null }) => {
return ( return (
<aside className={`ui-tooltip ${extraStyles}`}> <aside className={`ui-tooltip ${extraClasses}`}>
{ children } { children }
</aside> </aside>
) )

View File

@@ -6,7 +6,7 @@ import API from "../../util/API";
import Button from "./Button"; import Button from "./Button";
import Card from "./Card"; import Card from "./Card";
const UserCard: UserCardType = ({ extraStyles, targetUser }) => { const UserCard: UserCardType = ({ extraClasses, targetUser }) => {
const [buttonVariant, setButtonVariant] = useState(<></>); const [buttonVariant, setButtonVariant] = useState(<></>);
const { token } = useAuthContext(); const { token } = useAuthContext();
@@ -45,7 +45,7 @@ const UserCard: UserCardType = ({ extraStyles, targetUser }) => {
} }
return ( return (
<Card extraStyles={'user-card' + extraStyles}> <Card extraClasses={'user-card' + extraClasses}>
<> <>
<div className="avatar"></div> <div className="avatar"></div>
<h3><a href={`/profile?id=${targetUser.id}`}>{targetUser.firstname} {targetUser.lastname.substring(0,1)}.</a></h3> <h3><a href={`/profile?id=${targetUser.id}`}>{targetUser.firstname} {targetUser.lastname.substring(0,1)}.</a></h3>

View File

@@ -2,7 +2,7 @@ import Button from "./Button";
import Card from "./Card"; import Card from "./Card";
import Divider from "./Divider"; import Divider from "./Divider";
import Dropdown from "./Dropdown"; import Dropdown from "./Dropdown";
import Form from "./Form"; 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";

View File

@@ -7,7 +7,3 @@
text-align: center; text-align: center;
background-color: $richblack; background-color: $richblack;
} }
.ui-creatable-component {
margin-bottom: 10rem;
}

View File

@@ -5,7 +5,8 @@ import { IUser } from "../schemas";
export interface PortalBase { export interface PortalBase {
children?: ReactNode | ReactNode[] children?: ReactNode | ReactNode[]
extraStyles?: string extraClasses?: string
id?: string
} }
interface ButtonParams extends PortalBase { interface ButtonParams extends PortalBase {