diff --git a/web/src/pages/Create.tsx b/web/src/pages/Create.tsx index 19e3266..ff63ae1 100644 --- a/web/src/pages/Create.tsx +++ b/web/src/pages/Create.tsx @@ -1,6 +1,101 @@ +import { useEffect, useState } from "react"; import Banner from "../components/Banner"; +import { isRecipeMeal } from "../types/recipe"; + +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: string[]; + image: File | null; +}; + +interface CreateRecipeFormToggles { + title: boolean; + description: boolean; + prepTime: boolean; + cookTime: boolean; + servingSize: boolean; + category: boolean; + difficulty: boolean; + ingredients: boolean; + instructions: boolean; + // TODO: Image +} + +/** + * Classes which are applied to all of the input elements. + */ +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"; export default function Create() { + // FORM STATE + const [inputs, setInputs] = useState({ + title: "", + description: "", + tagInput: "", + tags: [], + prepTime: "", + cookTime: "", + servingSize: "", + category: "", + difficulty: "", + ingredients: [{ name: "", quantity: "" }], + instructions: [""], + image: null, + }); + + // VALIDATION STATE + const [validation, setValidation] = useState({ + title: true, + description: true, + prepTime: true, + cookTime: true, + servingSize: true, + category: true, + difficulty: true, + ingredients: true, + instructions: true, + }); + const [dirty, setDirty] = useState({ + title: false, + description: false, + prepTime: false, + cookTime: false, + servingSize: false, + category: false, + difficulty: false, + ingredients: false, + instructions: false, + }); + const [isFormValid, setIsFormValid] = useState(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.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.category = dirty.category ? (inputs.category !== "" && isRecipeMeal(inputs.category)) : true; + state.difficulty = dirty.difficulty ? (inputs.difficulty !== "" && Number(inputs.difficulty) >= 1 && Number(inputs.difficulty) <= 5) : true; + + setValidation(state); + } + + + // HANDLERS + // TODO: Only needed if we use the form element const keyDownHandler = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { e.preventDefault(); @@ -9,6 +104,32 @@ export default function Create() { return true; } + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setInputs(prev => ({ + ...prev, + [name]: value, + })); + setDirty(prev => ({ + ...prev, + [name]: true, + })); + }; + + // EFFECTS + useEffect(() => { + // Execute validation every time inputs change + validate(); + console.log("@inputs", inputs); + }, [inputs]); + + useEffect(() => { + // The form is only valid when every item has been touched, and every item is valid! + const allValid = Object.values(validation).every(x => x === true); + const allDirty = Object.values(dirty).every(x => x === true); + setIsFormValid(allValid && allDirty); + }, [validation, dirty]); + return ( <> @@ -22,7 +143,8 @@ export default function Create() { button to share your masterpiece!

-
+
+ {/* Title Input */}
+ + {/* Description Input */}
+ + {/* TODO: Tags Input */}
    + + {/* Time Input */}
    -
    -
    -
    + + {/* Dropdown Inputs */}
    + + {/* TODO: Ingredient Inputs */}
    @@ -260,7 +394,7 @@ export default function Create() { minLength={1} placeholder="Quantity (e.g., 1lb)" /> -

    +

    Please provide a quantity.

    @@ -273,6 +407,8 @@ export default function Create() { Add Ingredient + + {/* TODO: Instructions Inputs */}
    @@ -304,6 +440,8 @@ export default function Create() { Add Instruction Step + + {/* TODO: Images Input */}
    -

    - -
    + ); diff --git a/web/src/types/recipe.ts b/web/src/types/recipe.ts index 1f7bd53..5b338e7 100644 --- a/web/src/types/recipe.ts +++ b/web/src/types/recipe.ts @@ -5,7 +5,28 @@ export interface RecipeDuration { Cook: number; } -export type RecipeMeal = "breakfast" | "lunch" | "dinner" | "dessert" | "snack" | "side" | "other"; +export type RecipeMeal = + "breakfast" + | "lunch" + | "dinner" + | "dessert" + | "snack" + | "side" + | "other"; + +const RECIPE_MEALS = [ + "breakfast", + "lunch", + "dinner", + "dessert", + "snack", + "side", + "other" +]; + +export function isRecipeMeal(value: string): value is RecipeMeal { + return RECIPE_MEALS.includes(value as RecipeMeal); +} export interface RecipeIngredient { Name: string;