(FIX): Validation and dirty checks have been implemented.
Review this, but it looks right.
This commit is contained in:
parent
c729e883e0
commit
ce6d748731
@ -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}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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() !== ""
|
||||||
|
}
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user