Merging in the React Refactor #56

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

View File

@ -2,7 +2,6 @@ import { Reorder, useDragControls } from "motion/react";
import { INGREDIENT_UNITS, type RecipeIngredient } from "../../types/recipe"; import { INGREDIENT_UNITS, type RecipeIngredient } from "../../types/recipe";
import DeleteIconSmall from "../icons/DeleteIconSmall"; import DeleteIconSmall from "../icons/DeleteIconSmall";
import DragIconSmall from "../icons/DragIconSmall"; import DragIconSmall from "../icons/DragIconSmall";
import { useEffect, useState } from "react";
interface IngredientItemProps { interface IngredientItemProps {
classes: string; classes: string;
@ -10,35 +9,19 @@ interface IngredientItemProps {
onChange: (id: string, name: "Amount" | "Unit" | "Name", value: string) => void; onChange: (id: string, name: "Amount" | "Unit" | "Name", value: string) => void;
removeIngredientHandler: (id: string) => void; removeIngredientHandler: (id: string) => void;
allowDelete: boolean; allowDelete: boolean;
valid: boolean;
dirty: boolean;
markDirty: (id: string) => void;
} }
export default function IngredientItem({ classes, ingredient, onChange, removeIngredientHandler, allowDelete }: IngredientItemProps) { export default function IngredientItem({ classes, ingredient, onChange, removeIngredientHandler, allowDelete, valid, dirty, markDirty }: IngredientItemProps) {
const [dirty, setDirty] = useState<boolean>(false);
const [valid, setValid] = useState<boolean>(false);
const controls = useDragControls(); const controls = useDragControls();
// HANDLERS
const changeHandler = (name: "Amount" | "Unit" | "Name", value: string) => { const changeHandler = (name: "Amount" | "Unit" | "Name", value: string) => {
if (!dirty) setDirty(true); if (!dirty) markDirty(ingredient.Id);
onChange(ingredient.Id, name, value); onChange(ingredient.Id, name, value);
} }
useEffect(() => {
let _valid = true;
if (ingredient.Name.trim() === "") _valid = false;
if (ingredient.Unit === "") _valid = false;
if (ingredient.Amount <= 0) _valid = false;
setValid(_valid);
}, [ingredient]);
useEffect(() => {
console.log("@dirty", dirty);
}, [dirty]);
return ( return (
<Reorder.Item <Reorder.Item
key={ingredient.Id} key={ingredient.Id}

View File

@ -1,6 +1,7 @@
import { Reorder } from "motion/react"; import { Reorder } from "motion/react";
import type { RecipeIngredient, RecipeIngredientSection } from "../../types/recipe"; import type { RecipeIngredient, RecipeIngredientSection } from "../../types/recipe";
import IngredientItem from "./IngredientItem"; import IngredientItem from "./IngredientItem";
import type { RecipeValidationEntry } from "../../pages/Create";
interface IngredientListProps { interface IngredientListProps {
classes: string; classes: string;
@ -9,9 +10,12 @@ interface IngredientListProps {
setSectionIngredients: (sectionId: string, ingredients: RecipeIngredient[]) => void; setSectionIngredients: (sectionId: string, ingredients: RecipeIngredient[]) => void;
ingredientChangeHandler: (id: string, name: "Amount" | "Unit" | "Name", value: string) => void; ingredientChangeHandler: (id: string, name: "Amount" | "Unit" | "Name", value: string) => void;
removeIngredientHandler: (id: string) => void; removeIngredientHandler: (id: string) => void;
validList: RecipeValidationEntry[];
dirtyList: Record<string, boolean>;
markDirty: (id: string) => void;
} }
export default function IngredientList({ classes, section, ingredients, setSectionIngredients, ingredientChangeHandler, removeIngredientHandler }: IngredientListProps) { export default function IngredientList({ classes, section, ingredients, setSectionIngredients, ingredientChangeHandler, removeIngredientHandler, validList, dirtyList, markDirty }: IngredientListProps) {
const sectionIngredients = ingredients.filter(x => x.SectionId === section.Id); const sectionIngredients = ingredients.filter(x => x.SectionId === section.Id);
const reorderHandler = (ingredients: RecipeIngredient[]) => { const reorderHandler = (ingredients: RecipeIngredient[]) => {
@ -25,14 +29,17 @@ export default function IngredientList({ classes, section, ingredients, setSecti
onReorder={reorderHandler} onReorder={reorderHandler}
className="flex flex-col" className="flex flex-col"
> >
{sectionIngredients.map(ing => {sectionIngredients.map(ingredient =>
<IngredientItem <IngredientItem
key={ing.Id} key={ingredient.Id}
classes={classes} classes={classes}
ingredient={ing} ingredient={ingredient}
onChange={ingredientChangeHandler} onChange={ingredientChangeHandler}
removeIngredientHandler={removeIngredientHandler} removeIngredientHandler={removeIngredientHandler}
allowDelete={sectionIngredients.length > 1} allowDelete={sectionIngredients.length > 1}
valid={validList.find(x => x.id === ingredient.Id)?.valid ?? true}
dirty={dirtyList[ingredient.Id] ?? false}
markDirty={markDirty}
/> />
)} )}
</Reorder.Group> </Reorder.Group>

View File

@ -1,4 +1,4 @@
import { useEffect, useState, type ChangeEvent } from "react"; import { type ChangeEvent } from "react";
import type { RecipeInstruction } from "../../types/recipe"; import type { RecipeInstruction } from "../../types/recipe";
import { Reorder, useDragControls } from "motion/react"; import { Reorder, useDragControls } from "motion/react";
import DragIconSmall from "../icons/DragIconSmall"; import DragIconSmall from "../icons/DragIconSmall";
@ -10,28 +10,19 @@ interface InstructionElementProps {
allowDelete: boolean; allowDelete: boolean;
onChange: (id: string, value: string) => void; onChange: (id: string, value: string) => void;
onDelete: (id: string) => void; onDelete: (id: string) => void;
valid: boolean;
dirty: boolean;
markDirty: (id: string) => void;
} }
export default function InstructionElement({ instruction, index, allowDelete, onChange, onDelete }: InstructionElementProps) { export default function InstructionElement({ instruction, index, allowDelete, onChange, onDelete, valid, dirty, markDirty }: InstructionElementProps) {
const controls = useDragControls(); const controls = useDragControls();
const [valid, setValid] = useState<boolean>(true);
const [dirty, setDirty] = useState<boolean>(false);
// HANDLERS
const changeHandler = (e: ChangeEvent<HTMLTextAreaElement>) => { const changeHandler = (e: ChangeEvent<HTMLTextAreaElement>) => {
// No need to set many times if (!dirty) markDirty(instruction.Id)
if (!dirty) setDirty(true);
onChange(instruction.Id, e.target.value); onChange(instruction.Id, e.target.value);
} }
// EFFECTS
useEffect(() => {
if (dirty)
setValid(instruction.Content !== "");
}, [dirty, instruction]);
return ( return (
<Reorder.Item <Reorder.Item
value={instruction} value={instruction}
@ -43,7 +34,7 @@ export default function InstructionElement({ instruction, index, allowDelete, on
<h2 className="text-lg md:text-xl mr-4 text-gray-500">{index + 1}.</h2> <h2 className="text-lg md:text-xl mr-4 text-gray-500">{index + 1}.</h2>
<div className="flex flex-col flex-grow"> <div className="flex flex-col flex-grow">
<textarea <textarea
className="flex-grow 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 min-h-40 md:min-h-26 shadow-sm" className={`flex-grow 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 min-h-40 md:min-h-26 shadow-sm ${!valid && dirty ? "border-red-500" : ""}`}
name="instructions" name="instructions"
value={instruction.Content} value={instruction.Content}
onChange={changeHandler} onChange={changeHandler}
@ -52,7 +43,7 @@ export default function InstructionElement({ instruction, index, allowDelete, on
minLength={1} minLength={1}
placeholder="Describe this step..." placeholder="Describe this step..."
/> />
{!valid && ( {(!valid && dirty) && (
<p className="text-xs text-red-500 my-1"> <p className="text-xs text-red-500 my-1">
Please enter an instruction (blank entries are not allowed). Please enter an instruction (blank entries are not allowed).
</p> </p>

View File

@ -2,14 +2,18 @@ import { Reorder } from "motion/react";
import InstructionElement from "./InstructionElement"; import InstructionElement from "./InstructionElement";
import type { Dispatch, SetStateAction } from "react"; import type { Dispatch, SetStateAction } from "react";
import type { RecipeInstruction } from "../../types/recipe"; import type { RecipeInstruction } from "../../types/recipe";
import type { RecipeValidationEntry } from "../../pages/Create";
interface InstructionListProps { interface InstructionListProps {
instructions: RecipeInstruction[]; instructions: RecipeInstruction[];
setInstructions: Dispatch<SetStateAction<RecipeInstruction[]>>; setInstructions: Dispatch<SetStateAction<RecipeInstruction[]>>;
validList: RecipeValidationEntry[];
dirtyList: Record<string, boolean>;
markDirty: (id: string) => void;
} }
export default function InstructionList({ instructions, setInstructions }: InstructionListProps) { export default function InstructionList({ instructions, setInstructions, validList, dirtyList, markDirty }: InstructionListProps) {
const handleChange = (id: string, value: string) => { const handleChange = (id: string, value: string) => {
setInstructions(prev => setInstructions(prev =>
prev.map(instr => prev.map(instr =>
@ -39,6 +43,9 @@ export default function InstructionList({ instructions, setInstructions }: Instr
allowDelete={instructions.length > 1} allowDelete={instructions.length > 1}
onChange={handleChange} onChange={handleChange}
onDelete={handleDelete} onDelete={handleDelete}
valid={validList.find(x => x.id === instruction.Id)?.valid ?? true}
dirty={dirtyList[instruction.Id] ?? false}
markDirty={markDirty}
/> />
))} ))}
</Reorder.Group> </Reorder.Group>

View File

@ -29,6 +29,16 @@ export default function ValidationErrorList({ validation }: ValidationErrorListP
</p> </p>
); );
})} })}
{validation.ingredients.filter(x => !x.valid).length > 0 && (
<p className="text-sm text-red-500">
{MESSAGES.ingredients}
</p>
)}
{validation.instructions.filter(x => !x.valid).length > 0 && (
<p className="text-sm text-red-500">
{MESSAGES.instructions}
</p>
)}
</div> </div>
); );
} }

View File

@ -1,5 +1,5 @@
import { useEffect, type ChangeEvent, type Dispatch, type SetStateAction } from "react"; import { type ChangeEvent, type Dispatch, type SetStateAction } from "react";
import type { CreateRecipeFormEntries } from "../../pages/Create"; import type { CreateRecipeFormDirtyEntries } from "../../pages/Create";
export interface RecipeCreateDropdownOption { export interface RecipeCreateDropdownOption {
value: string; value: string;
@ -14,7 +14,7 @@ interface RecipeCreateFormDropdownProps {
valid: boolean; valid: boolean;
value: string; value: string;
setValue: Dispatch<SetStateAction<string>>; setValue: Dispatch<SetStateAction<string>>;
setDirty: Dispatch<SetStateAction<CreateRecipeFormEntries>>; setDirty: Dispatch<SetStateAction<CreateRecipeFormDirtyEntries>>;
options: RecipeCreateDropdownOption[]; options: RecipeCreateDropdownOption[];
error: string; error: string;
parentClasses?: string; parentClasses?: string;
@ -27,10 +27,6 @@ export default function RecipeCreateDropdownInput({ label, name, desc, required
setValue(e.target.value); setValue(e.target.value);
} }
useEffect(() => {
console.debug(`@${name}`, value);
}, [name, value]);
return ( return (
<div className={`flex flex-col ${parentClasses}`}> <div className={`flex flex-col ${parentClasses}`}>
<label htmlFor={name} className="text-sm"> <label htmlFor={name} className="text-sm">

View File

@ -1,5 +1,5 @@
import { useEffect, type ChangeEvent, type Dispatch, type InputHTMLAttributes, type SetStateAction } from "react"; import { useEffect, type ChangeEvent, type Dispatch, type InputHTMLAttributes, type SetStateAction } from "react";
import type { CreateRecipeFormEntries } from "../../pages/Create"; import type { CreateRecipeFormDirtyEntries } from "../../pages/Create";
interface RecipeCreateFormInputProps interface RecipeCreateFormInputProps
extends Omit< extends Omit<
@ -15,23 +15,18 @@ interface RecipeCreateFormInputProps
valid: boolean; valid: boolean;
value: string; value: string;
setValue: Dispatch<SetStateAction<string>>; setValue: Dispatch<SetStateAction<string>>;
setDirty: Dispatch<SetStateAction<CreateRecipeFormEntries>>; setDirty: Dispatch<SetStateAction<CreateRecipeFormDirtyEntries>>;
error: string; error: string;
parentClasses?: string; parentClasses?: string;
classes: string; classes: string;
}; };
export default function RecipeCreateFormInput({ label, name, desc, placeholder, type = "text", required = false, valid, value, setDirty, setValue, error, parentClasses = "", classes, ...inputProps }: RecipeCreateFormInputProps) { 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>) => { const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
setDirty(prev => ({...prev, [name]: true})); setDirty(prev => ({ ...prev, [name]: true }));
setValue(e.target.value); setValue(e.target.value);
} }
useEffect(() => {
console.debug(`@${name}`, value);
}, [name, value]);
return ( return (
<div className={`flex flex-col ${parentClasses}`}> <div className={`flex flex-col ${parentClasses}`}>
<label htmlFor={name} className="text-sm"> <label htmlFor={name} className="text-sm">

View File

@ -1,5 +1,5 @@
import { useEffect, type ChangeEvent, type Dispatch, type SetStateAction, type TextareaHTMLAttributes } from "react"; import { type ChangeEvent, type Dispatch, type SetStateAction, type TextareaHTMLAttributes } from "react";
import type { CreateRecipeFormEntries } from "../../pages/Create"; import type { CreateRecipeFormDirtyEntries } from "../../pages/Create";
interface RecipeCreateFormInputProps interface RecipeCreateFormInputProps
extends Omit< extends Omit<
@ -14,7 +14,7 @@ interface RecipeCreateFormInputProps
valid: boolean; valid: boolean;
value: string; value: string;
setValue: Dispatch<SetStateAction<string>>; setValue: Dispatch<SetStateAction<string>>;
setDirty: Dispatch<SetStateAction<CreateRecipeFormEntries>>; setDirty: Dispatch<SetStateAction<CreateRecipeFormDirtyEntries>>;
error: string; error: string;
parentClasses?: string; parentClasses?: string;
classes: string; classes: string;
@ -26,10 +26,6 @@ export default function RecipeCreateFormTextArea({ label, name, desc, placeholde
setValue(e.target.value); setValue(e.target.value);
} }
useEffect(() => {
console.debug(`@${name}`, value);
}, [name, value]);
return ( return (
<div className={`flex flex-col ${parentClasses}`}> <div className={`flex flex-col ${parentClasses}`}>
<label htmlFor={name} className="text-sm"> <label htmlFor={name} className="text-sm">

View File

@ -1,5 +1,5 @@
import type { CreateRecipeFormEntries } from "../pages/Create"; import type { CreateRecipeFormDirtyEntries, CreateRecipeFormEntries } from "../pages/Create";
import { isRecipeMeal } from "../types/recipe"; import { isRecipeMeal, type RecipeIngredient, type RecipeInstruction } from "../types/recipe";
export interface CreateRecipeFormValues { export interface CreateRecipeFormValues {
title: string; title: string;
@ -9,9 +9,11 @@ export interface CreateRecipeFormValues {
servingSize: string; servingSize: string;
category: string; category: string;
difficulty: string; difficulty: string;
ingredients: RecipeIngredient[];
instructions: RecipeInstruction[];
} }
export function validateCreateRecipeForm(values: CreateRecipeFormValues, dirty: CreateRecipeFormEntries): CreateRecipeFormEntries { export function validateCreateRecipeForm(values: CreateRecipeFormValues, dirty: CreateRecipeFormDirtyEntries): CreateRecipeFormEntries {
return { return {
title: dirty.title title: dirty.title
? values.title.length >= 1 && values.title.length <= 128 ? values.title.length >= 1 && values.title.length <= 128
@ -42,7 +44,24 @@ export function validateCreateRecipeForm(values: CreateRecipeFormValues, dirty:
Number(values.difficulty) >= 1 && Number(values.difficulty) >= 1 &&
Number(values.difficulty) <= 5 Number(values.difficulty) <= 5
: true, : true,
ingredients: true, ingredients: values.ingredients.map(ingredient => {
instructions: true, if (!dirty.ingredients[ingredient.Id]) {
return { id: ingredient.Id, valid: true };
}
let valid = true;
if (ingredient.Name.trim() === "") valid = false;
if (ingredient.Unit === "") valid = false;
if (ingredient.Amount <= 0) valid = false;
return { id: ingredient.Id, valid };
}),
instructions: values.instructions.map(instruction => {
if (!dirty.instructions[instruction.Id]) {
return { id: instruction.Id, valid: true }
}
return {
id: instruction.Id,
valid: instruction.Content.trim() !== ""
}
}),
} }
} }

View File

@ -14,7 +14,12 @@ import RecipeCreateFormTagsInputs from "../components/inputs/RecipeCreateFormTag
import { useIngredients } from "../hooks/useIngredients"; import { useIngredients } from "../hooks/useIngredients";
import { validateCreateRecipeForm } from "../hooks/validation"; import { validateCreateRecipeForm } from "../hooks/validation";
// TODO: Move this // TODO: Move these
export interface RecipeValidationEntry {
id: string;
valid: boolean;
}
export interface CreateRecipeFormEntries { export interface CreateRecipeFormEntries {
title: boolean; title: boolean;
description: boolean; description: boolean;
@ -23,8 +28,21 @@ export interface CreateRecipeFormEntries {
servingSize: boolean; servingSize: boolean;
category: boolean; category: boolean;
difficulty: boolean; difficulty: boolean;
ingredients: boolean; ingredients: RecipeValidationEntry[];
instructions: boolean; instructions: RecipeValidationEntry[];
// TODO: Image
}
export interface CreateRecipeFormDirtyEntries {
title: boolean;
description: boolean;
prepTime: boolean;
cookTime: boolean;
servingSize: boolean;
category: boolean;
difficulty: boolean;
ingredients: Record<string, boolean>;
instructions: Record<string, boolean>;
// TODO: Image // TODO: Image
} }
@ -82,13 +100,13 @@ export default function Create() {
servingSize: true, servingSize: true,
category: true, category: true,
difficulty: true, difficulty: true,
ingredients: true, ingredients: [],
instructions: true, instructions: [],
}); });
const [isFormValid, setIsFormValid] = useState<boolean>(false); const [isFormValid, setIsFormValid] = useState<boolean>(false);
// Dirty State // Dirty State
const [dirty, setDirty] = useState<CreateRecipeFormEntries>({ const [dirty, setDirty] = useState<CreateRecipeFormDirtyEntries>({
title: false, title: false,
description: false, description: false,
prepTime: false, prepTime: false,
@ -96,8 +114,8 @@ export default function Create() {
servingSize: false, servingSize: false,
category: false, category: false,
difficulty: false, difficulty: false,
ingredients: true, // Can this be ignored since they're self contained? ingredients: {},
instructions: true, // Can this be ignored since they're self contained? instructions: {},
}); });
// Import ingredients // Import ingredients
@ -119,22 +137,90 @@ export default function Create() {
setInstructions([...instructions, { Id: crypto.randomUUID(), Content: "" }]); setInstructions([...instructions, { Id: crypto.randomUUID(), Content: "" }]);
} }
// Dirty handlers
const markInstructionDirty = (id: string) => {
setDirty(prev => ({
...prev,
instructions: {
...prev.instructions,
[id]: true,
},
}));
};
const markIngredientDirty = (id: string) => {
setDirty(prev => ({
...prev,
ingredients: {
...prev.ingredients,
[id]: true,
},
}));
};
const markAllIngredientsDirty = (): Record<string, boolean> => {
const all: Record<string, boolean> = {};
for (const ing of ingredients) all[ing.Id] = true;
return all;
};
const markAllInstructionsDirty = (): Record<string, boolean> => {
const all: Record<string, boolean> = {};
for (const instr of instructions) all[instr.Id] = true;
return all;
};
// HANDLERS
const submitHandler = () => {
// If any inputs are not dirty, simply dirty them all and return
const scalar_dirty = [
dirty.title,
dirty.description,
dirty.prepTime,
dirty.cookTime,
dirty.servingSize,
dirty.category,
dirty.difficulty,
];
const ingredients_dirty = Object.values(dirty.ingredients).every(Boolean);
const instructions_dirty = Object.values(dirty.instructions).every(Boolean);
const all_dirty = scalar_dirty.every(Boolean) && ingredients_dirty && instructions_dirty;
if (!all_dirty) {
setDirty({
title: true,
description: true,
prepTime: true,
cookTime: true,
servingSize: true,
category: true,
difficulty: true,
ingredients: markAllIngredientsDirty(),
instructions: markAllInstructionsDirty(),
});
return;
}
}
// EFFECTS // EFFECTS
useEffect(() => { useEffect(() => {
// Execute validation every time inputs change // Execute validation every time inputs change
setValidation( setValidation(
validateCreateRecipeForm( validateCreateRecipeForm(
{ title, description, prepTime, cookTime, servingSize, category, difficulty }, { title, description, prepTime, cookTime, servingSize, category, difficulty, ingredients, instructions },
dirty dirty
) )
); );
}, [title, description, prepTime, cookTime, servingSize, category, difficulty, instructions, ingredients, dirty]); }, [title, description, prepTime, cookTime, servingSize, category, difficulty, instructions, ingredients, dirty]);
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 is valid!
const allValid = Object.values(validation).every(x => x === true); const bools_valid = Object.values(validation).filter(x => typeof x === "boolean").every(x => x);
const allDirty = Object.values(dirty).every(x => x === true); const ingredients_valid = validation.ingredients.filter(x => !x.valid).length === 0;
setIsFormValid(allValid && allDirty); const instructions_valid = validation.instructions.filter(x => !x.valid).length === 0;
setIsFormValid(bools_valid && ingredients_valid && instructions_valid);
}, [validation, dirty]); }, [validation, dirty]);
useEffect(() => { useEffect(() => {
@ -291,7 +377,7 @@ export default function Create() {
/> />
</div> </div>
{/* TODO: Ingredient Inputs */} {/* Ingredient Inputs */}
<RecipeCreateFormWrapper <RecipeCreateFormWrapper
label="Ingredients" label="Ingredients"
name="ingredients" name="ingredients"
@ -320,6 +406,9 @@ export default function Create() {
setSectionIngredients={setSectionIngredients} setSectionIngredients={setSectionIngredients}
ingredientChangeHandler={ingredientChange} ingredientChangeHandler={ingredientChange}
removeIngredientHandler={removeIngredient} removeIngredientHandler={removeIngredient}
validList={validation.ingredients}
dirtyList={dirty.ingredients}
markDirty={markIngredientDirty}
/> />
<button <button
onClick={() => addIngredient(section.Id)} onClick={() => addIngredient(section.Id)}
@ -350,6 +439,9 @@ export default function Create() {
<InstructionList <InstructionList
instructions={instructions} instructions={instructions}
setInstructions={setInstructions} setInstructions={setInstructions}
validList={validation.instructions}
dirtyList={dirty.instructions}
markDirty={markInstructionDirty}
/> />
<button <button
onClick={addInstructionHandler} onClick={addInstructionHandler}
@ -380,7 +472,11 @@ export default function Create() {
{/* Display the reason for the invalidation */} {/* Display the reason for the invalidation */}
<ValidationErrorList validation={validation} /> <ValidationErrorList validation={validation} />
<button disabled={!isFormValid} className={`${isFormValid ? "bg-gradient-to-r from-blue-200 to-purple-200 cursor-pointer" : "bg-gray-200 text-gray-500 cursor-not-allowed"} w-full py-2 rounded-lg text-lg shadow-md`}> <button
onClick={submitHandler}
disabled={!isFormValid}
className={`${isFormValid ? "bg-gradient-to-r from-blue-200 to-purple-200 cursor-pointer" : "bg-gray-200 text-gray-500 cursor-not-allowed"} w-full py-2 rounded-lg text-lg shadow-md`}
>
Create Recipe Create Recipe
</button> </button>
</div> </div>