(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.
This commit is contained in:
Hayden Hargreaves 2025-12-17 23:03:59 -07:00
parent 357e8772e7
commit deecc01c7e
7 changed files with 502 additions and 333 deletions

View File

@ -1,10 +1,10 @@
import type { CreateRecipeFormToggles } from "../../pages/Create"; import type { CreateRecipeFormEntries } from "../../pages/Create";
interface ValidationErrorListProps { interface ValidationErrorListProps {
validation: CreateRecipeFormToggles; validation: CreateRecipeFormEntries;
} }
const MESSAGES: Record<keyof CreateRecipeFormToggles, string> = { const MESSAGES: Record<keyof CreateRecipeFormEntries, string> = {
title: "Invalid title provided.", title: "Invalid title provided.",
description: "Invalid description provided.", description: "Invalid description provided.",
prepTime: "Invalid preparation time provided.", prepTime: "Invalid preparation time provided.",
@ -22,7 +22,7 @@ export default function ValidationErrorList({ validation }: ValidationErrorListP
{Object.entries(validation) {Object.entries(validation)
.filter(([, isValid]) => !isValid) .filter(([, isValid]) => !isValid)
.map(([name]) => { .map(([name]) => {
const key = name as keyof CreateRecipeFormToggles; const key = name as keyof CreateRecipeFormEntries;
return ( return (
<p key={name} className="text-sm text-red-500"> <p key={name} className="text-sm text-red-500">
{MESSAGES[key]} {MESSAGES[key]}

View File

@ -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<SetStateAction<string>>;
setDirty: Dispatch<SetStateAction<CreateRecipeFormEntries>>;
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<HTMLSelectElement>) => {
setDirty(prev => ({ ...prev, [name]: true }));
setValue(e.target.value);
}
useEffect(() => {
console.debug(`@${name}`, value);
}, [name, value]);
return (
<div className={`flex flex-col ${parentClasses}`}>
<label htmlFor={name} className="text-sm">
{label}
{required && <span className="text-red-500">*</span>}
</label>
<p className="text-xs pt-1 pb-2 text-gray-700">
{desc}
</p>
<select
className={`${!valid ? "border-red-500" : ""} ${classes}`}
name={name}
value={value}
onChange={handleChange}
required={required}
>
{options?.map(opt => (
<option key={opt.value} value={opt.value}>{opt.name}</option>
))}
</select>
{!valid && (
<p className="text-xs text-red-500 my-1">
{error}
</p>
)}
</div>
);
}

View File

@ -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<HTMLInputElement>,
"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<SetStateAction<string>>;
setDirty: Dispatch<SetStateAction<CreateRecipeFormEntries>>;
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<HTMLInputElement>) => {
setDirty(prev => ({...prev, [name]: true}));
setValue(e.target.value);
}
useEffect(() => {
console.debug(`@${name}`, value);
}, [name, value]);
return (
<div className={`flex flex-col ${parentClasses}`}>
<label htmlFor={name} className="text-sm">
{label}
{required && <span className="text-red-500">*</span>}
</label>
<p className="text-xs pt-1 pb-2 text-gray-700">
{desc}
</p>
<input
className={`${!valid ? "border-red-500" : ""} ${classes}`}
type={type}
name={name}
value={value}
onChange={handleChange}
required={required}
placeholder={placeholder}
{...inputProps}
/>
{!valid && (
<p className="text-xs text-red-500 my-1">
{error}
</p>
)}
</div>
);
}

View File

@ -0,0 +1,70 @@
import { useState, type ChangeEvent, type Dispatch, type FormEvent, type SetStateAction } from "react";
interface RecipeCreateFormTagsInputsProps {
tags: string[];
setTags: Dispatch<SetStateAction<string[]>>;
classes: string;
}
export default function RecipeCreateFormTagsInputs({ tags, setTags, classes }: RecipeCreateFormTagsInputsProps) {
const [input, setInput] = useState<string>("");
const changeHandler = (e: ChangeEvent<HTMLInputElement>) => setInput(e.target.value);
const tagCreationHandler = (e: FormEvent<HTMLFormElement>) => {
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 (
<form onSubmit={tagCreationHandler} className="my-4 flex flex-col gap-x-2">
<div className="flex flex-col flex-grow">
<label htmlFor="tags" className="text-sm">
Recipe Tags
</label>
<p className="text-xs pt-1 pb-2 text-gray-700">
Please provide a list of tags. <span className="italic">e.g., easy, dairy-free, gluten-free, high protein.</span>
</p>
<input
type="text"
value={input}
onChange={changeHandler}
name="tagInput"
maxLength={32}
enterKeyHint="done"
placeholder="e.g., Healthy"
className={classes}
/>
<input type="hidden" name="tags" id="tags" value="" />
</div>
<ul id="tag-list" className="my-2 flex gap-1 flex-wrap">
{tags?.map(tag =>
<li
className="flex text-xs items-center bg-blue-100 w-fit px-2 py-1 rounded-full gap-x-1 select-none cursor-pointer hover:bg-blue-200 duration-300"
key={tag}
>
<button tabIndex={-1} type="button" onClick={() => tagDeletionHandler(tag)}>
&times; {tag}
</button>
</li>
)}
</ul>
</form>
);
}

View File

@ -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<HTMLTextAreaElement>,
"value" | "onChange" | "name" | "type" | "placeholder" | "required"
> {
label: string;
name: string;
desc: string;
placeholder: string;
required?: boolean;
valid: boolean;
value: string;
setValue: Dispatch<SetStateAction<string>>;
setDirty: Dispatch<SetStateAction<CreateRecipeFormEntries>>;
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<HTMLTextAreaElement>) => {
setDirty(prev => ({...prev, [name]: true}));
setValue(e.target.value);
}
useEffect(() => {
console.debug(`@${name}`, value);
}, [name, value]);
return (
<div className={`flex flex-col ${parentClasses}`}>
<label htmlFor={name} className="text-sm">
{label}
{required && <span className="text-red-500">*</span>}
</label>
<p className="text-xs pt-1 pb-2 text-gray-700">
{desc}
</p>
<textarea
className={`${!valid ? "border-red-500" : ""} ${classes}`}
name={name}
value={value}
onChange={handleChange}
required={required}
placeholder={placeholder}
{...inputProps}
/>
{!valid && (
<p className="text-xs text-red-500 my-1">
{error}
</p>
)}
</div>
);
}

View File

@ -0,0 +1,28 @@
import type { ReactNode } from "react";
interface RecipeCreateFormWrapperProps {
label: string;
name: string;
desc: string;
required: boolean;
parentClasses: string;
children: ReactNode;
}
export default function RecipeCreateFormWrapper({label, name, desc, required = false, parentClasses, children }: RecipeCreateFormWrapperProps) {
const normalized_name = name.toLowerCase().replaceAll(" ", "-");
return (
<div className={`flex flex-col ${parentClasses}`}>
<label htmlFor={normalized_name} className="text-sm">
{label}
{required && <span className="text-red-500">*</span>}
</label>
<p className="text-xs py-1 text-gray-700">
{desc}
</p>
{children}
</div>
);
}

View File

@ -1,4 +1,4 @@
import { useEffect, useState, type ChangeEvent, type FormEvent } from "react"; import { useEffect, useState, type FormEvent } from "react";
import Banner from "../components/Banner"; import Banner from "../components/Banner";
import { isRecipeMeal, type RecipeIngredient, type RecipeIngredientSection, type RecipeIngredientUnit, type RecipeInstruction } from "../types/recipe"; import { isRecipeMeal, type RecipeIngredient, type RecipeIngredientSection, type RecipeIngredientUnit, type RecipeInstruction } from "../types/recipe";
import InstructionList from "../components/forms/InstructionList"; import InstructionList from "../components/forms/InstructionList";
@ -6,25 +6,14 @@ import ValidationErrorList from "../components/forms/ValidationErrorList";
import IngredientSection from "../components/forms/IngredientSection"; import IngredientSection from "../components/forms/IngredientSection";
import { Reorder } from "motion/react"; import { Reorder } from "motion/react";
import IngredientList from "../components/forms/IngredientList"; import IngredientList from "../components/forms/IngredientList";
import RecipeCreateFormInput from "../components/inputs/RecipeCreateFormInput";
import RecipeCreateDropdownInput, { type RecipeCreateDropdownOption } from "../components/inputs/RecipeCreateFormDropdown";
import RecipeCreateFormTextArea from "../components/inputs/RecipeCreateFormTextArea";
import RecipeCreateFormWrapper from "../components/inputs/RecipeCreateFormWrapper";
import RecipeCreateFormTagsInputs from "../components/inputs/RecipeCreateFormTagsInput";
// TODO: Move this // TODO: Move this
interface CreateRecipeForm { export interface CreateRecipeFormEntries {
title: string;
description: string;
tagInput: string; // current tag being typed
tags: string[]; // all tags
prepTime: number | "";
cookTime: number | "";
servingSize: number | "";
category: string;
difficulty: string; // We use this as a number...
ingredients: { name: string; quantity: string }[];
// Instructions are stored elsewhere
image: File | null;
};
// TODO: Move this
export interface CreateRecipeFormToggles {
title: boolean; title: boolean;
description: boolean; description: boolean;
prepTime: boolean; prepTime: boolean;
@ -42,36 +31,54 @@ export interface CreateRecipeFormToggles {
*/ */
const INPUT_CLASSES = "border border-gray-300 px-4 py-2 rounded-lg focus:outline-none focus:ring-blue-500 focus:ring-2 duration-200 ease-in-out transition-all shadow-sm"; const INPUT_CLASSES = "border border-gray-300 px-4 py-2 rounded-lg focus:outline-none focus:ring-blue-500 focus:ring-2 duration-200 ease-in-out transition-all shadow-sm";
/**
* Options passed to the category dropdown.
*/
const CATEGORY_OPTIONS: RecipeCreateDropdownOption[] = [
{ value: "", name: "Select a category" },
{ value: "breakfast", name: "Breakfast" },
{ value: "lunch", name: "Lunch" },
{ value: "dinner", name: "Dinner" },
{ value: "dessert", name: "Dessert" },
{ value: "snack", name: "Snack" },
{ value: "side", name: "Side" },
{ value: "other", name: "Other" },
];
/**
* Options passed to the difficulty dropdown.
*/
const DIFFICULTY_OPTIONS: RecipeCreateDropdownOption[] = [
{ value: "", name: "Select a difficulty" },
{ value: "1", name: "Beginner" },
{ value: "2", name: "Easy" },
{ value: "3", name: "Intermediate" },
{ value: "4", name: "Challenging" },
{ value: "5", name: "Extreme" },
];
export default function Create() { export default function Create() {
// FORM STATE // Inputs
const [inputs, setInputs] = useState<CreateRecipeForm>({ const [title, setTitle] = useState<string>("");
title: "", const [description, setDescription] = useState<string>("");
description: "", const [tags, setTags] = useState<string[]>([]);
tagInput: "", const [prepTime, setPrepTime] = useState<string>("");
tags: [], const [cookTime, setCookTime] = useState<string>("");
prepTime: "", const [servingSize, setServingSize] = useState<string>("");
cookTime: "", const [category, setCategory] = useState<string>("");
servingSize: "", const [difficulty, setDifficulty] = useState<string>("");
category: "",
difficulty: "",
ingredients: [{ name: "", quantity: "" }],
image: null,
});
// Store complex values elsewhere
const [instructions, setInstructions] = useState<RecipeInstruction[]>([ const [instructions, setInstructions] = useState<RecipeInstruction[]>([
{ Id: crypto.randomUUID(), Content: "" } { Id: crypto.randomUUID(), Content: "" }
]); ]);
const [sections, setSections] = useState<RecipeIngredientSection[]>([ const [sections, setSections] = useState<RecipeIngredientSection[]>([
{ Id: "initial-section", Name: "Unnamed group" }, { Id: "initial-section", Name: "Unnamed group" },
]); ]);
const [ingredients, setIngredients] = useState<RecipeIngredient[]>([ const [ingredients, setIngredients] = useState<RecipeIngredient[]>([
{ Id: crypto.randomUUID(), SectionId: "initial-section", Name: "", Amount: 0, Unit: "" }, { Id: crypto.randomUUID(), SectionId: "initial-section", Name: "", Amount: 0, Unit: "" },
]); ]);
// VALIDATION STATE // Validation State
const [validation, setValidation] = useState<CreateRecipeFormToggles>({ const [validation, setValidation] = useState<CreateRecipeFormEntries>({
title: true, title: true,
description: true, description: true,
prepTime: true, prepTime: true,
@ -82,7 +89,10 @@ export default function Create() {
ingredients: true, ingredients: true,
instructions: true, instructions: true,
}); });
const [dirty, setDirty] = useState<CreateRecipeFormToggles>({ const [isFormValid, setIsFormValid] = useState<boolean>(false);
// Dirty State
const [dirty, setDirty] = useState<CreateRecipeFormEntries>({
title: false, title: false,
description: false, description: false,
prepTime: false, prepTime: false,
@ -93,74 +103,31 @@ export default function Create() {
ingredients: true, // This can be ignored since they're self contained ingredients: true, // This can be ignored since they're self contained
instructions: true, // This we can ignore since they're self contained instructions: true, // This we can ignore since they're self contained
}); });
const [isFormValid, setIsFormValid] = useState<boolean>(false);
// Validate the dirty inputs, one at a time // Validate the dirty inputs, one at a time
const validate = () => { const validate = () => {
const state = { ...validation }; const state = { ...validation };
state.title = dirty.title ? (inputs.title.length >= 1 && inputs.title.length <= 128) : true; state.title = dirty.title ? (title.length >= 1 && title.length <= 128) : true;
state.description = dirty.description ? (inputs.description.length >= 1 && inputs.description.length <= 1000) : true; state.description = dirty.description ? (description.length >= 1 && description.length <= 1000) : true;
state.prepTime = dirty.prepTime ? (inputs.prepTime !== "" && Number(inputs.prepTime) >= 0 && Number(inputs.prepTime) <= 120) : true; state.prepTime = dirty.prepTime ? (prepTime !== "" && Number(prepTime) >= 0 && Number(prepTime) <= 300) : true;
state.cookTime = dirty.cookTime ? (inputs.cookTime !== "" && Number(inputs.cookTime) >= 0 && Number(inputs.cookTime) <= 120) : true; state.cookTime = dirty.cookTime ? (cookTime !== "" && Number(cookTime) >= 0 && Number(cookTime) <= 300) : true;
state.servingSize = dirty.servingSize ? (inputs.servingSize !== "" && Number(inputs.servingSize) >= 1 && Number(inputs.servingSize) <= 16) : true; state.servingSize = dirty.servingSize ? (servingSize !== "" && Number(servingSize) >= 1 && Number(servingSize) <= 16) : true;
state.category = dirty.category ? (inputs.category !== "" && isRecipeMeal(inputs.category)) : true; state.category = dirty.category ? (category !== "" && isRecipeMeal(category)) : true;
state.difficulty = dirty.difficulty ? (inputs.difficulty !== "" && Number(inputs.difficulty) >= 1 && Number(inputs.difficulty) <= 5) : true; state.difficulty = dirty.difficulty ? (difficulty !== "" && Number(difficulty) >= 1 && Number(difficulty) <= 5) : true;
// state.instructions = instructions?.filter(x => x.content === "").length === 0; // All of them are not empty
// TODO: How do I validate the instructions and ingredients
setValidation(state); setValidation(state);
} }
// HANDLERS // Instruction handlers
const changeHandler = (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
const { name, value } = e.target;
setInputs(prev => ({
...prev,
[name]: value,
}));
setDirty(prev => ({
...prev,
[name]: true,
}));
};
const tagCreationHandler = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
// why would anyone try this lol
if (inputs.tagInput.trim() === "") return;
// Tag already exists, clear input and exit
if (inputs.tags.includes(inputs.tagInput.toLowerCase())) {
setInputs(prev => ({
...prev,
tagInput: "",
}));
return;
}
setInputs(prev => ({
...prev,
tags: [...prev.tags, inputs.tagInput.toLowerCase()],
tagInput: "",
}));
}
const tagDeletionHandler = (tag: string) => {
if (!tag) return;
setInputs(prev => ({
...prev,
tags: prev.tags.filter(t => t !== tag)
}));
}
const addInstructionHandler = () => { const addInstructionHandler = () => {
setInstructions([...instructions, { Id: crypto.randomUUID(), Content: "" }]); setInstructions([...instructions, { Id: crypto.randomUUID(), Content: "" }]);
} }
// Ingredient Handlers
const sectionChangeHandler = (id: string, name: string) => { const sectionChangeHandler = (id: string, name: string) => {
setSections(prev => setSections(prev =>
prev.map(section => prev.map(section =>
@ -230,7 +197,7 @@ export default function Create() {
useEffect(() => { useEffect(() => {
// Execute validation every time inputs change // Execute validation every time inputs change
validate(); validate();
}, [inputs, instructions, ingredients]); }, [title, description, prepTime, cookTime, servingSize, category, difficulty, instructions, ingredients]);
useEffect(() => { useEffect(() => {
// The form is only valid when every item has been touched, and every item is valid! // The form is only valid when every item has been touched, and every item is valid!
@ -239,6 +206,14 @@ export default function Create() {
setIsFormValid(allValid && allDirty); setIsFormValid(allValid && allDirty);
}, [validation, dirty]); }, [validation, dirty]);
useEffect(() => {
console.debug("@validation", validation);
}, [validation]);
useEffect(() => {
console.debug("@dirty", dirty);
}, [dirty]);
return ( return (
<> <>
<Banner content="Create Your Masterpiece" /> <Banner content="Create Your Masterpiece" />
@ -254,235 +229,145 @@ export default function Create() {
</p> </p>
<div> <div>
{/* Title Input */} {/* Title Input */}
<div className="flex flex-col"> <RecipeCreateFormInput
<label htmlFor="title" className="text-sm"> label="Recipe Title"
Recipe Title name="title"
<span className="text-red-500">*</span> desc="Please provide a unique title for your recipe. This is the most important part!"
</label> placeholder="e.g., Classic Chicken Curry"
<p className="text-xs pt-1 pb-2 text-gray-700"> required
Please provide a unique title for your recipe. This is the most important part! valid={validation.title}
</p> value={title}
<input setValue={setTitle}
className={`${!validation.title ? "border-red-500" : ""} ${INPUT_CLASSES}`} setDirty={setDirty}
type="text" maxLength={128}
name="title" error="Please enter a title. Between 1-128 characters."
value={inputs.title} classes={INPUT_CLASSES}
onChange={changeHandler} />
required
maxLength={128}
minLength={1}
placeholder="e.g., Classic Chicken Curry"
/>
{!validation.title && (
<p className="text-xs text-red-500 my-1">Please enter a title. Between 1-128 characters.</p>
)}
</div>
{/* Description Input */} {/* Description Input */}
<div className="flex flex-col my-4"> <RecipeCreateFormTextArea
<label htmlFor="description" className="text-sm"> label="Description"
Description name="description"
<span className="text-red-500">*</span> desc="Please provide a description for your recipe. This can be short and sweet or long and detailed!"
</label> placeholder="A brief description of your delicious recipe..."
<p className="text-xs pt-1 pb-2 text-gray-700"> required
Please provide a description for your recipe. This can be short and sweet or long and detailed! valid={validation.description}
</p> value={description}
<textarea setValue={setDescription}
className={`${!validation.description ? "border-red-500" : ""} ${INPUT_CLASSES} min-h-32`} setDirty={setDirty}
name="description" error="Please enter a description. Between 1-1000 characters."
value={inputs.description} rows={4}
onChange={changeHandler} maxLength={1024}
rows={4} minLength={1}
required parentClasses="my-4"
maxLength={1024} classes={INPUT_CLASSES}
minLength={1} />
placeholder="A brief description of your delicious recipe..."
></textarea>
{!validation.description && (
<p className="text-xs text-red-500 my-1">
Please enter a description. Between 1-1000 characters.
</p>
)}
</div>
{/* Tag Input */} {/* Tag Input */}
<form onSubmit={tagCreationHandler} className="my-4 flex flex-col gap-x-2"> <RecipeCreateFormTagsInputs
<div className="flex flex-col flex-grow"> tags={tags}
<label htmlFor="tags" className="text-sm"> setTags={setTags}
Recipe Tags classes={INPUT_CLASSES}
</label> />
<p className="text-xs pt-1 pb-2 text-gray-700">
Please provide a list of tags. <span className="italic">e.g., easy, dairy-free, gluten-free, high protein.</span>
</p>
<input
type="text"
value={inputs.tagInput}
onChange={changeHandler}
name="tagInput"
maxLength={32}
enterKeyHint="done"
placeholder="e.g., Healthy"
className="border border-gray-300 px-4 py-2 rounded-lg focus:outline-none focus:ring-blue-500 focus:ring-2 duration-200 ease-in-out transition-all shadow-sm"
/>
<input type="hidden" name="tags" id="tags" value="" />
</div>
<ul id="tag-list" className="my-2 flex gap-1 flex-wrap">
{inputs.tags.map(tag =>
<li
className="flex text-xs items-center bg-blue-100 w-fit px-2 py-1 rounded-full gap-x-1 select-none cursor-pointer hover:bg-blue-200 duration-300"
key={tag}
>
<button tabIndex={-1} type="button" onClick={() => tagDeletionHandler(tag)}>
&times; {tag}
</button>
</li>
)}
</ul>
</form>
{/* Time Input */} {/* Time Input */}
<div className="my-4 flex gap-x-2"> <div className="my-4 flex gap-x-2">
<div className="flex flex-col flex-grow w-1/3"> <RecipeCreateFormInput
<label htmlFor="prepTime" className="text-sm"> label="Prep Time"
Prep Time name="prepTime"
<span className="text-red-500">*</span> type="number"
</label> desc="Please provide the estimated prep time (minutes)."
<p className="text-xs pt-1 pb-2 text-gray-700"> placeholder="e.g., 20"
Please provide the estimated prep time (minutes). required
</p> valid={validation.prepTime}
<input value={prepTime}
className={`${!validation.prepTime ? "border-red-500" : ""} ${INPUT_CLASSES}`} setValue={setPrepTime}
type="number" setDirty={setDirty}
name="prepTime" error="Please enter a time (minutes)."
value={inputs.prepTime} parentClasses="flex-grow w-1/3"
onChange={changeHandler} min="0"
required max="300"
min="0" classes={INPUT_CLASSES}
max="120" />
placeholder="e.g., 20"
/> <RecipeCreateFormInput
{!validation.prepTime && ( label="Cook Time"
<p className="text-xs text-red-500 my-1"> Please enter a time (minutes). </p> name="cookTime"
)} type="number"
</div> desc="Please provide the estimated cook time (minutes)."
<div className="flex flex-col flex-grow w-1/3"> placeholder="e.g., 45"
<label htmlFor="cookTime" className="text-sm"> required
Cook Time valid={validation.cookTime}
<span className="text-red-500">*</span> value={cookTime}
</label> setValue={setCookTime}
<p className="text-xs pt-1 pb-2 text-gray-700"> setDirty={setDirty}
Please provide the estimated cook time (minutes). error="Please enter a time (minutes)."
</p> parentClasses="flex-grow w-1/3"
<input min="0"
className={`${!validation.cookTime ? "border-red-500" : ""} ${INPUT_CLASSES}`} max="300"
type="number" classes={INPUT_CLASSES}
name="cookTime" />
value={inputs.cookTime}
onChange={changeHandler} <RecipeCreateFormInput
required label="Serving Size"
min="0" name="servingSize"
max="120" type="number"
placeholder="e.g., 45" desc="Please provide the estimated serving size."
/> placeholder="e.g., 4"
{!validation.cookTime && ( required
<p className="text-xs text-red-500 my-1"> Please enter a time (minutes). </p> valid={validation.servingSize}
)} value={servingSize}
</div> setValue={setServingSize}
<div className="flex flex-col flex-grow w-1/3"> setDirty={setDirty}
<label htmlFor="servingSize" className="text-sm"> error="Please enter a serving size."
Serving Size parentClasses="flex-grow w-1/3"
<span className="text-red-500">*</span> min="1"
</label> max="16"
<p className="text-xs pt-1 pb-2 text-gray-700"> classes={INPUT_CLASSES}
Please provide the estimated serving size. />
</p>
<input
className={`${!validation.servingSize ? "border-red-500" : ""} ${INPUT_CLASSES}`}
type="number"
name="servingSize"
value={inputs.servingSize}
onChange={changeHandler}
max="16"
min="1"
required
placeholder="e.g., 4"
/>
{!validation.servingSize && (
<p className="text-xs text-red-500 my-1"> Please enter a serving size. </p>
)}
</div>
</div> </div>
{/* Dropdown Inputs */} {/* Dropdown Inputs */}
<div className="my-4 flex gap-x-2"> <div className="my-4 flex gap-x-2">
<div className="flex flex-col flex-grow w-1/3"> <RecipeCreateDropdownInput
<label htmlFor="category" className="text-sm"> label="Category"
Category name="category"
<span className="text-red-500">*</span> desc="Please provide the meal category."
</label> required
<p className="text-xs pt-1 pb-2 text-gray-700"> valid={validation.category}
Please provide the meal category. value={category}
</p> setValue={setCategory}
<select setDirty={setDirty}
className={`${!validation.category ? "border-red-500" : ""} ${INPUT_CLASSES}`} options={CATEGORY_OPTIONS}
name="category" error="Please select a category."
value={inputs.category} parentClasses="flex-grow w-1/3"
onChange={changeHandler} classes={INPUT_CLASSES}
required />
>
<option value="">Select a category</option> <RecipeCreateDropdownInput
<option value="breakfast">Breakfast</option> label="Difficulty"
<option value="lunch">Lunch</option> name="difficulty"
<option value="dinner">Dinner</option> desc="Please provide a baseline difficulty."
<option value="dessert">Dessert</option> required
<option value="snack">Snack</option> valid={validation.difficulty}
<option value="side">Side</option> value={difficulty}
<option value="other">Other</option> setValue={setDifficulty}
</select> setDirty={setDirty}
{!validation.category && ( options={DIFFICULTY_OPTIONS}
<p className="text-xs text-red-500 my-1"> Please select a category. </p> error="Please select a difficulty."
)} parentClasses="flex-grow w-1/3"
</div> classes={INPUT_CLASSES}
<div className="flex flex-col flex-grow w-1/3"> />
<label htmlFor="difficulty" className="text-sm">
Difficulty
<span className="text-red-500">*</span>
</label>
<p className="text-xs pt-1 pb-2 text-gray-700">
Please provide a baseline difficulty.
</p>
<select
className={`${!validation.category ? "border-red-500" : ""} ${INPUT_CLASSES}`}
name="difficulty"
value={inputs.difficulty}
onChange={changeHandler}
required
>
<option value="">Select a difficulty</option>
<option value="1">Beginner</option>
<option value="2">Easy</option>
<option value="3">Intermediate</option>
<option value="4">Challenging</option>
<option value="5">Extreme</option>
</select>
{!validation.difficulty && (
<p className="text-xs text-red-500 my-1"> Please select a difficulty. </p>
)}
</div>
</div> </div>
{/* TODO: Ingredient Inputs */} {/* TODO: Ingredient Inputs */}
<div className="flex flex-col my-4"> <RecipeCreateFormWrapper
<label htmlFor="ingredients" className="text-sm"> label="Ingredients"
Ingredients name="ingredients"
<span className="text-red-500">*</span> desc="Please provide a list of ingredients and their quantities."
</label> required
<p className="text-xs py-1 text-gray-700"> parentClasses="my-4"
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.
</p>
<Reorder.Group <Reorder.Group
axis="y" axis="y"
values={sections} values={sections}
@ -497,7 +382,6 @@ export default function Create() {
removeIngredientSectionHandler={removeIngredientSectionHandler} removeIngredientSectionHandler={removeIngredientSectionHandler}
allowDelete={sections.length > 1} allowDelete={sections.length > 1}
> >
<IngredientList <IngredientList
classes={INPUT_CLASSES} classes={INPUT_CLASSES}
section={section} section={section}
@ -521,28 +405,29 @@ export default function Create() {
</IngredientSection> </IngredientSection>
))} ))}
</Reorder.Group> </Reorder.Group>
</RecipeCreateFormWrapper>
</div>
{/* Instruction Inputs */} {/* Instruction Inputs */}
<div className="flex flex-col my-4"> <RecipeCreateFormWrapper
<label htmlFor="instructions" className="text-sm"> label="Instructions"
Instructions name="instructions"
<span className="text-red-500">*</span> desc="Please provide a list of instructions. You do not need to include step number, they will be added automatically!"
</label> required
<p className="text-xs py-1 text-gray-700"> parentClasses="my-4"
Please provide a list of instructions. You do not need to include step number, they will be added automatically! >
</p> <>
<InstructionList
<InstructionList instructions={instructions} setInstructions={setInstructions} /> instructions={instructions}
setInstructions={setInstructions}
<button />
onClick={addInstructionHandler} <button
className="w-fit px-3 py-1.5 text-blue-800 font-semibold text-xs md:text-sm rounded-lg hover:bg-gray-100 duration-300 cursor-pointer" onClick={addInstructionHandler}
> className="w-fit px-3 py-1.5 text-blue-800 font-semibold text-xs md:text-sm rounded-lg hover:bg-gray-100 duration-300 cursor-pointer"
Add Instruction Step >
</button> Add Instruction Step
</div> </button>
</>
</RecipeCreateFormWrapper>
{/* TODO: Images Input */} {/* TODO: Images Input */}
<div className="flex flex-col my-4"> <div className="flex flex-col my-4">