(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 {
validation: CreateRecipeFormToggles;
validation: CreateRecipeFormEntries;
}
const MESSAGES: Record<keyof CreateRecipeFormToggles, string> = {
const MESSAGES: Record<keyof CreateRecipeFormEntries, string> = {
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 (
<p key={name} className="text-sm text-red-500">
{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 { isRecipeMeal, type RecipeIngredient, type RecipeIngredientSection, type RecipeIngredientUnit, type RecipeInstruction } from "../types/recipe";
import InstructionList from "../components/forms/InstructionList";
@ -6,25 +6,14 @@ import ValidationErrorList from "../components/forms/ValidationErrorList";
import IngredientSection from "../components/forms/IngredientSection";
import { Reorder } from "motion/react";
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
interface CreateRecipeForm {
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 {
export interface CreateRecipeFormEntries {
title: boolean;
description: 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";
/**
* 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() {
// FORM STATE
const [inputs, setInputs] = useState<CreateRecipeForm>({
title: "",
description: "",
tagInput: "",
tags: [],
prepTime: "",
cookTime: "",
servingSize: "",
category: "",
difficulty: "",
ingredients: [{ name: "", quantity: "" }],
image: null,
});
// Store complex values elsewhere
// Inputs
const [title, setTitle] = useState<string>("");
const [description, setDescription] = useState<string>("");
const [tags, setTags] = useState<string[]>([]);
const [prepTime, setPrepTime] = useState<string>("");
const [cookTime, setCookTime] = useState<string>("");
const [servingSize, setServingSize] = useState<string>("");
const [category, setCategory] = useState<string>("");
const [difficulty, setDifficulty] = useState<string>("");
const [instructions, setInstructions] = useState<RecipeInstruction[]>([
{ Id: crypto.randomUUID(), Content: "" }
]);
const [sections, setSections] = useState<RecipeIngredientSection[]>([
{ Id: "initial-section", Name: "Unnamed group" },
]);
const [ingredients, setIngredients] = useState<RecipeIngredient[]>([
{ Id: crypto.randomUUID(), SectionId: "initial-section", Name: "", Amount: 0, Unit: "" },
]);
// VALIDATION STATE
const [validation, setValidation] = useState<CreateRecipeFormToggles>({
// Validation State
const [validation, setValidation] = useState<CreateRecipeFormEntries>({
title: true,
description: true,
prepTime: true,
@ -82,7 +89,10 @@ export default function Create() {
ingredients: true,
instructions: true,
});
const [dirty, setDirty] = useState<CreateRecipeFormToggles>({
const [isFormValid, setIsFormValid] = useState<boolean>(false);
// Dirty State
const [dirty, setDirty] = useState<CreateRecipeFormEntries>({
title: false,
description: false,
prepTime: false,
@ -93,74 +103,31 @@ export default function Create() {
ingredients: true, // This can be ignored 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
const validate = () => {
const state = { ...validation };
state.title = dirty.title ? (inputs.title.length >= 1 && inputs.title.length <= 128) : true;
state.description = dirty.description ? (inputs.description.length >= 1 && inputs.description.length <= 1000) : true;
state.title = dirty.title ? (title.length >= 1 && title.length <= 128) : 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.cookTime = dirty.cookTime ? (inputs.cookTime !== "" && Number(inputs.cookTime) >= 0 && Number(inputs.cookTime) <= 120) : true;
state.servingSize = dirty.servingSize ? (inputs.servingSize !== "" && Number(inputs.servingSize) >= 1 && Number(inputs.servingSize) <= 16) : true;
state.prepTime = dirty.prepTime ? (prepTime !== "" && Number(prepTime) >= 0 && Number(prepTime) <= 300) : true;
state.cookTime = dirty.cookTime ? (cookTime !== "" && Number(cookTime) >= 0 && Number(cookTime) <= 300) : true;
state.servingSize = dirty.servingSize ? (servingSize !== "" && Number(servingSize) >= 1 && Number(servingSize) <= 16) : true;
state.category = dirty.category ? (inputs.category !== "" && isRecipeMeal(inputs.category)) : true;
state.difficulty = dirty.difficulty ? (inputs.difficulty !== "" && Number(inputs.difficulty) >= 1 && Number(inputs.difficulty) <= 5) : true;
// state.instructions = instructions?.filter(x => x.content === "").length === 0; // All of them are not empty
state.category = dirty.category ? (category !== "" && isRecipeMeal(category)) : true;
state.difficulty = dirty.difficulty ? (difficulty !== "" && Number(difficulty) >= 1 && Number(difficulty) <= 5) : true;
// TODO: How do I validate the instructions and ingredients
setValidation(state);
}
// 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)
}));
}
// Instruction handlers
const addInstructionHandler = () => {
setInstructions([...instructions, { Id: crypto.randomUUID(), Content: "" }]);
}
// Ingredient Handlers
const sectionChangeHandler = (id: string, name: string) => {
setSections(prev =>
prev.map(section =>
@ -230,7 +197,7 @@ export default function Create() {
useEffect(() => {
// Execute validation every time inputs change
validate();
}, [inputs, instructions, ingredients]);
}, [title, description, prepTime, cookTime, servingSize, category, difficulty, instructions, ingredients]);
useEffect(() => {
// 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);
}, [validation, dirty]);
useEffect(() => {
console.debug("@validation", validation);
}, [validation]);
useEffect(() => {
console.debug("@dirty", dirty);
}, [dirty]);
return (
<>
<Banner content="Create Your Masterpiece" />
@ -254,235 +229,145 @@ export default function Create() {
</p>
<div>
{/* Title Input */}
<div className="flex flex-col">
<label htmlFor="title" className="text-sm">
Recipe Title
<span className="text-red-500">*</span>
</label>
<p className="text-xs pt-1 pb-2 text-gray-700">
Please provide a unique title for your recipe. This is the most important part!
</p>
<input
className={`${!validation.title ? "border-red-500" : ""} ${INPUT_CLASSES}`}
type="text"
name="title"
value={inputs.title}
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>
<RecipeCreateFormInput
label="Recipe Title"
name="title"
desc="Please provide a unique title for your recipe. This is the most important part!"
placeholder="e.g., Classic Chicken Curry"
required
valid={validation.title}
value={title}
setValue={setTitle}
setDirty={setDirty}
maxLength={128}
error="Please enter a title. Between 1-128 characters."
classes={INPUT_CLASSES}
/>
{/* Description Input */}
<div className="flex flex-col my-4">
<label htmlFor="description" className="text-sm">
Description
<span className="text-red-500">*</span>
</label>
<p className="text-xs pt-1 pb-2 text-gray-700">
Please provide a description for your recipe. This can be short and sweet or long and detailed!
</p>
<textarea
className={`${!validation.description ? "border-red-500" : ""} ${INPUT_CLASSES} min-h-32`}
name="description"
value={inputs.description}
onChange={changeHandler}
rows={4}
required
maxLength={1024}
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>
<RecipeCreateFormTextArea
label="Description"
name="description"
desc="Please provide a description for your recipe. This can be short and sweet or long and detailed!"
placeholder="A brief description of your delicious recipe..."
required
valid={validation.description}
value={description}
setValue={setDescription}
setDirty={setDirty}
error="Please enter a description. Between 1-1000 characters."
rows={4}
maxLength={1024}
minLength={1}
parentClasses="my-4"
classes={INPUT_CLASSES}
/>
{/* Tag Input */}
<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={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>
<RecipeCreateFormTagsInputs
tags={tags}
setTags={setTags}
classes={INPUT_CLASSES}
/>
{/* Time Input */}
<div className="my-4 flex gap-x-2">
<div className="flex flex-col flex-grow w-1/3">
<label htmlFor="prepTime" className="text-sm">
Prep Time
<span className="text-red-500">*</span>
</label>
<p className="text-xs pt-1 pb-2 text-gray-700">
Please provide the estimated prep time (minutes).
</p>
<input
className={`${!validation.prepTime ? "border-red-500" : ""} ${INPUT_CLASSES}`}
type="number"
name="prepTime"
value={inputs.prepTime}
onChange={changeHandler}
required
min="0"
max="120"
placeholder="e.g., 20"
/>
{!validation.prepTime && (
<p className="text-xs text-red-500 my-1"> Please enter a time (minutes). </p>
)}
</div>
<div className="flex flex-col flex-grow w-1/3">
<label htmlFor="cookTime" className="text-sm">
Cook Time
<span className="text-red-500">*</span>
</label>
<p className="text-xs pt-1 pb-2 text-gray-700">
Please provide the estimated cook time (minutes).
</p>
<input
className={`${!validation.cookTime ? "border-red-500" : ""} ${INPUT_CLASSES}`}
type="number"
name="cookTime"
value={inputs.cookTime}
onChange={changeHandler}
required
min="0"
max="120"
placeholder="e.g., 45"
/>
{!validation.cookTime && (
<p className="text-xs text-red-500 my-1"> Please enter a time (minutes). </p>
)}
</div>
<div className="flex flex-col flex-grow w-1/3">
<label htmlFor="servingSize" className="text-sm">
Serving Size
<span className="text-red-500">*</span>
</label>
<p className="text-xs pt-1 pb-2 text-gray-700">
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>
<RecipeCreateFormInput
label="Prep Time"
name="prepTime"
type="number"
desc="Please provide the estimated prep time (minutes)."
placeholder="e.g., 20"
required
valid={validation.prepTime}
value={prepTime}
setValue={setPrepTime}
setDirty={setDirty}
error="Please enter a time (minutes)."
parentClasses="flex-grow w-1/3"
min="0"
max="300"
classes={INPUT_CLASSES}
/>
<RecipeCreateFormInput
label="Cook Time"
name="cookTime"
type="number"
desc="Please provide the estimated cook time (minutes)."
placeholder="e.g., 45"
required
valid={validation.cookTime}
value={cookTime}
setValue={setCookTime}
setDirty={setDirty}
error="Please enter a time (minutes)."
parentClasses="flex-grow w-1/3"
min="0"
max="300"
classes={INPUT_CLASSES}
/>
<RecipeCreateFormInput
label="Serving Size"
name="servingSize"
type="number"
desc="Please provide the estimated serving size."
placeholder="e.g., 4"
required
valid={validation.servingSize}
value={servingSize}
setValue={setServingSize}
setDirty={setDirty}
error="Please enter a serving size."
parentClasses="flex-grow w-1/3"
min="1"
max="16"
classes={INPUT_CLASSES}
/>
</div>
{/* Dropdown Inputs */}
<div className="my-4 flex gap-x-2">
<div className="flex flex-col flex-grow w-1/3">
<label htmlFor="category" className="text-sm">
Category
<span className="text-red-500">*</span>
</label>
<p className="text-xs pt-1 pb-2 text-gray-700">
Please provide the meal category.
</p>
<select
className={`${!validation.category ? "border-red-500" : ""} ${INPUT_CLASSES}`}
name="category"
value={inputs.category}
onChange={changeHandler}
required
>
<option value="">Select a category</option>
<option value="breakfast">Breakfast</option>
<option value="lunch">Lunch</option>
<option value="dinner">Dinner</option>
<option value="dessert">Dessert</option>
<option value="snack">Snack</option>
<option value="side">Side</option>
<option value="other">Other</option>
</select>
{!validation.category && (
<p className="text-xs text-red-500 my-1"> Please select a category. </p>
)}
</div>
<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>
<RecipeCreateDropdownInput
label="Category"
name="category"
desc="Please provide the meal category."
required
valid={validation.category}
value={category}
setValue={setCategory}
setDirty={setDirty}
options={CATEGORY_OPTIONS}
error="Please select a category."
parentClasses="flex-grow w-1/3"
classes={INPUT_CLASSES}
/>
<RecipeCreateDropdownInput
label="Difficulty"
name="difficulty"
desc="Please provide a baseline difficulty."
required
valid={validation.difficulty}
value={difficulty}
setValue={setDifficulty}
setDirty={setDirty}
options={DIFFICULTY_OPTIONS}
error="Please select a difficulty."
parentClasses="flex-grow w-1/3"
classes={INPUT_CLASSES}
/>
</div>
{/* TODO: Ingredient Inputs */}
<div className="flex flex-col my-4">
<label htmlFor="ingredients" className="text-sm">
Ingredients
<span className="text-red-500">*</span>
</label>
<p className="text-xs py-1 text-gray-700">
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>
<RecipeCreateFormWrapper
label="Ingredients"
name="ingredients"
desc="Please provide a list of ingredients and their quantities."
required
parentClasses="my-4"
>
<Reorder.Group
axis="y"
values={sections}
@ -497,7 +382,6 @@ export default function Create() {
removeIngredientSectionHandler={removeIngredientSectionHandler}
allowDelete={sections.length > 1}
>
<IngredientList
classes={INPUT_CLASSES}
section={section}
@ -521,28 +405,29 @@ export default function Create() {
</IngredientSection>
))}
</Reorder.Group>
</div>
</RecipeCreateFormWrapper>
{/* Instruction Inputs */}
<div className="flex flex-col my-4">
<label htmlFor="instructions" className="text-sm">
Instructions
<span className="text-red-500">*</span>
</label>
<p className="text-xs py-1 text-gray-700">
Please provide a list of instructions. You do not need to include step number, they will be added automatically!
</p>
<InstructionList instructions={instructions} setInstructions={setInstructions} />
<button
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>
</div>
<RecipeCreateFormWrapper
label="Instructions"
name="instructions"
desc="Please provide a list of instructions. You do not need to include step number, they will be added automatically!"
required
parentClasses="my-4"
>
<>
<InstructionList
instructions={instructions}
setInstructions={setInstructions}
/>
<button
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>
</>
</RecipeCreateFormWrapper>
{/* TODO: Images Input */}
<div className="flex flex-col my-4">