From deecc01c7e9a9f5f42afd78db71d6134f1b2fad8 Mon Sep 17 00:00:00 2001 From: Hayden Hargreaves Date: Wed, 17 Dec 2025 23:03:59 -0700 Subject: [PATCH] (REFACTOR): Yet another rewrite... The validation has been kept in the parent Create.tsx file, and the inputs have been moved out to their owns components. The instructions and ingredients need validation. --- .../components/forms/ValidationErrorList.tsx | 8 +- .../inputs/RecipeCreateFormDropdown.tsx | 63 ++ .../inputs/RecipeCreateFormInput.tsx | 63 ++ .../inputs/RecipeCreateFormTagsInput.tsx | 70 +++ .../inputs/RecipeCreateFormTextArea.tsx | 60 ++ .../inputs/RecipeCreateFormWrapper.tsx | 28 + web/src/pages/Create.tsx | 543 +++++++----------- 7 files changed, 502 insertions(+), 333 deletions(-) create mode 100644 web/src/components/inputs/RecipeCreateFormDropdown.tsx create mode 100644 web/src/components/inputs/RecipeCreateFormInput.tsx create mode 100644 web/src/components/inputs/RecipeCreateFormTagsInput.tsx create mode 100644 web/src/components/inputs/RecipeCreateFormTextArea.tsx create mode 100644 web/src/components/inputs/RecipeCreateFormWrapper.tsx diff --git a/web/src/components/forms/ValidationErrorList.tsx b/web/src/components/forms/ValidationErrorList.tsx index a10c3ca..9fc029b 100644 --- a/web/src/components/forms/ValidationErrorList.tsx +++ b/web/src/components/forms/ValidationErrorList.tsx @@ -1,10 +1,10 @@ -import type { CreateRecipeFormToggles } from "../../pages/Create"; +import type { CreateRecipeFormEntries } from "../../pages/Create"; interface ValidationErrorListProps { - validation: CreateRecipeFormToggles; + validation: CreateRecipeFormEntries; } -const MESSAGES: Record = { +const MESSAGES: Record = { title: "Invalid title provided.", description: "Invalid description provided.", prepTime: "Invalid preparation time provided.", @@ -22,7 +22,7 @@ export default function ValidationErrorList({ validation }: ValidationErrorListP {Object.entries(validation) .filter(([, isValid]) => !isValid) .map(([name]) => { - const key = name as keyof CreateRecipeFormToggles; + const key = name as keyof CreateRecipeFormEntries; return (

{MESSAGES[key]} diff --git a/web/src/components/inputs/RecipeCreateFormDropdown.tsx b/web/src/components/inputs/RecipeCreateFormDropdown.tsx new file mode 100644 index 0000000..0655eff --- /dev/null +++ b/web/src/components/inputs/RecipeCreateFormDropdown.tsx @@ -0,0 +1,63 @@ +import { useEffect, type ChangeEvent, type Dispatch, type SetStateAction } from "react"; +import type { CreateRecipeFormEntries } from "../../pages/Create"; + +export interface RecipeCreateDropdownOption { + value: string; + name: string; +} + +interface RecipeCreateFormDropdownProps { + label: string; + name: string; + desc: string; + required?: boolean; + valid: boolean; + value: string; + setValue: Dispatch>; + setDirty: Dispatch>; + options: RecipeCreateDropdownOption[]; + error: string; + parentClasses?: string; + classes: string; +}; + +export default function RecipeCreateDropdownInput({ label, name, desc, required = false, valid, value, setDirty, setValue, options, error, parentClasses = "", classes }: RecipeCreateFormDropdownProps) { + const handleChange = (e: ChangeEvent) => { + setDirty(prev => ({ ...prev, [name]: true })); + setValue(e.target.value); + } + + useEffect(() => { + console.debug(`@${name}`, value); + }, [name, value]); + + return ( +

+ +

+ {desc} +

+ + {!valid && ( +

+ {error} +

+ )} +
+ ); +} + + diff --git a/web/src/components/inputs/RecipeCreateFormInput.tsx b/web/src/components/inputs/RecipeCreateFormInput.tsx new file mode 100644 index 0000000..c5b934a --- /dev/null +++ b/web/src/components/inputs/RecipeCreateFormInput.tsx @@ -0,0 +1,63 @@ +import { useEffect, type ChangeEvent, type Dispatch, type InputHTMLAttributes, type SetStateAction } from "react"; +import type { CreateRecipeFormEntries } from "../../pages/Create"; + +interface RecipeCreateFormInputProps + extends Omit< + InputHTMLAttributes, + "value" | "onChange" | "name" | "type" | "placeholder" | "required" + > { + label: string; + name: string; // ENSURE THE NAME MATCHES THE VALUE IN THE ENTRIES TYPE + desc: string; + placeholder: string; + type?: string; + required?: boolean; + valid: boolean; + value: string; + setValue: Dispatch>; + setDirty: Dispatch>; + error: string; + parentClasses?: string; + classes: string; +}; + +export default function RecipeCreateFormInput({ label, name, desc, placeholder, type = "text", required = false, valid, value, setDirty, setValue, error, parentClasses = "", classes, ...inputProps }: RecipeCreateFormInputProps) { + + const handleChange = (e: ChangeEvent) => { + setDirty(prev => ({...prev, [name]: true})); + setValue(e.target.value); + } + + useEffect(() => { + console.debug(`@${name}`, value); + }, [name, value]); + + return ( +
+ +

+ {desc} +

+ + {!valid && ( +

+ {error} +

+ )} +
+ ); +} + + diff --git a/web/src/components/inputs/RecipeCreateFormTagsInput.tsx b/web/src/components/inputs/RecipeCreateFormTagsInput.tsx new file mode 100644 index 0000000..690105d --- /dev/null +++ b/web/src/components/inputs/RecipeCreateFormTagsInput.tsx @@ -0,0 +1,70 @@ +import { useState, type ChangeEvent, type Dispatch, type FormEvent, type SetStateAction } from "react"; + +interface RecipeCreateFormTagsInputsProps { + tags: string[]; + setTags: Dispatch>; + classes: string; +} + +export default function RecipeCreateFormTagsInputs({ tags, setTags, classes }: RecipeCreateFormTagsInputsProps) { + const [input, setInput] = useState(""); + + const changeHandler = (e: ChangeEvent) => setInput(e.target.value); + + const tagCreationHandler = (e: FormEvent) => { + e.preventDefault(); + + // why would anyone try this lol + if (input.trim() === "") return; + + // Tag already exists, clear input and exit + if (tags.includes(input.toLowerCase())) { + setInput(""); + return; + } + + setInput(""); + setTags(prev => [...prev, input.toLowerCase()]); + } + + const tagDeletionHandler = (tag: string) => { + if (!tag) return; + setTags(prev => prev.filter(t => t !== tag)); + } + + return ( +
+
+ +

+ Please provide a list of tags. e.g., easy, dairy-free, gluten-free, high protein. +

+ + +
+
    + {tags?.map(tag => +
  • + +
  • + )} +
+
+ ); +} diff --git a/web/src/components/inputs/RecipeCreateFormTextArea.tsx b/web/src/components/inputs/RecipeCreateFormTextArea.tsx new file mode 100644 index 0000000..d31a5df --- /dev/null +++ b/web/src/components/inputs/RecipeCreateFormTextArea.tsx @@ -0,0 +1,60 @@ +import { useEffect, type ChangeEvent, type Dispatch, type InputHTMLAttributes, type SetStateAction, type TextareaHTMLAttributes } from "react"; +import type { CreateRecipeFormEntries } from "../../pages/Create"; + +interface RecipeCreateFormInputProps + extends Omit< + TextareaHTMLAttributes, + "value" | "onChange" | "name" | "type" | "placeholder" | "required" + > { + label: string; + name: string; + desc: string; + placeholder: string; + required?: boolean; + valid: boolean; + value: string; + setValue: Dispatch>; + setDirty: Dispatch>; + error: string; + parentClasses?: string; + classes: string; +}; + +export default function RecipeCreateFormTextArea({ label, name, desc, placeholder, required = false, valid, value, setDirty, setValue, error, parentClasses = "", classes, ...inputProps }: RecipeCreateFormInputProps) { + const handleChange = (e: ChangeEvent) => { + setDirty(prev => ({...prev, [name]: true})); + setValue(e.target.value); + } + + useEffect(() => { + console.debug(`@${name}`, value); + }, [name, value]); + + return ( +
+ +

+ {desc} +

+ - {!validation.description && ( -

- Please enter a description. Between 1-1000 characters. -

- )} -
+ {/* Tag Input */} -
-
- -

- Please provide a list of tags. e.g., easy, dairy-free, gluten-free, high protein. -

- - -
-
    - {inputs.tags.map(tag => -
  • - -
  • - )} -
-
+ {/* Time Input */}
-
- -

- Please provide the estimated prep time (minutes). -

- - {!validation.prepTime && ( -

Please enter a time (minutes).

- )} -
-
- -

- Please provide the estimated cook time (minutes). -

- - {!validation.cookTime && ( -

Please enter a time (minutes).

- )} -
-
- -

- Please provide the estimated serving size. -

- - {!validation.servingSize && ( -

Please enter a serving size.

- )} -
+ + + + +
{/* Dropdown Inputs */}
-
- -

- Please provide the meal category. -

- - {!validation.category && ( -

Please select a category.

- )} -
-
- -

- Please provide a baseline difficulty. -

- - {!validation.difficulty && ( -

Please select a difficulty.

- )} -
+ + +
{/* TODO: Ingredient Inputs */} -
- -

- Please provide a list of ingredients and their quantities. Ingredients can be added into - groups. If only a single group exists, the group will be ignored when the recipe is created - and the ingredients will appear as a single list. -

- + 1} > - ))} - -
+ {/* Instruction Inputs */} -
- -

- Please provide a list of instructions. You do not need to include step number, they will be added automatically! -

- - - - -
+ + <> + + + + {/* TODO: Images Input */}