Merging in the React Refactor #56

Merged
azpect merged 51 commits from refactor/react into master 2025-12-28 22:27:52 -07:00
7 changed files with 502 additions and 333 deletions
Showing only changes of commit deecc01c7e - Show all commits

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">