import { useEffect, useState } from "react"; import Banner from "../components/Banner"; import { isRecipeMeal, type RecipeInstruction } from "../types/recipe"; import InstructionList from "../components/forms/InstructionList"; 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"; import { useIngredients } from "../hooks/useIngredients"; import { validateCreateRecipeForm } from "../hooks/validation"; import { CreateRecipe } from "../services/RecipeService"; import type { CreateRecipeRequest } from "../types/api/recipe"; import { isApiError } from "../types/api/error"; import { useNavigate } from "react-router-dom"; import ROUTE_CONSTANTS from "../types/routes"; // TODO: Move these export interface RecipeValidationEntry { id: string; valid: boolean; } export interface CreateRecipeFormEntries { title: boolean; description: boolean; prepTime: boolean; cookTime: boolean; servingSize: boolean; category: boolean; difficulty: boolean; ingredients: RecipeValidationEntry[]; instructions: RecipeValidationEntry[]; // TODO: Image } export interface CreateRecipeFormDirtyEntries { title: boolean; description: boolean; prepTime: boolean; cookTime: boolean; servingSize: boolean; category: boolean; difficulty: boolean; ingredients: Record; instructions: Record; // 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"; /** * 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" }, ]; export default function Create() { // Inputs const [title, setTitle] = useState(""); const [description, setDescription] = useState(""); const [tags, setTags] = useState([]); const [prepTime, setPrepTime] = useState(""); const [cookTime, setCookTime] = useState(""); const [servingSize, setServingSize] = useState(""); const [category, setCategory] = useState(""); const [difficulty, setDifficulty] = useState(""); const [instructions, setInstructions] = useState([ { Id: crypto.randomUUID(), Content: "" } ]); // Validation State const [validation, setValidation] = useState({ title: true, description: true, prepTime: true, cookTime: true, servingSize: true, category: true, difficulty: true, ingredients: [], instructions: [], }); const [isFormValid, setIsFormValid] = useState(false); // Dirty State const [dirty, setDirty] = useState({ title: false, description: false, prepTime: false, cookTime: false, servingSize: false, category: false, difficulty: false, ingredients: {}, instructions: {}, }); const navigate = useNavigate(); // Functions const createRecipe = async (): Promise => { console.log({ title, description, tags, prepTime, cookTime, servingSize, category, difficulty, sections, ingredients, instructions }); // Exit if not valid recipe meal if (!isRecipeMeal(category)) { console.error("[ERROR] Recipe meal is invalid."); return; } const recipe: CreateRecipeRequest = { Title: title, Description: description, Instructions: instructions, Serves: Number(servingSize), Difficulty: Number(difficulty), Duration: { Prep: Number(prepTime), Cook: Number(cookTime), Total: Number(prepTime) + Number(cookTime) }, Category: category, Ingredients: ingredients, Sections: sections, Tags: tags, }; const response = await CreateRecipe(recipe); if (isApiError(response)) { console.error(response); return; } // TODO: Success toast! await navigate(ROUTE_CONSTANTS.Recipe(response.Id)); }; // Import ingredients const { sections, ingredients, setSections, sectionChange, ingredientChange, setSectionIngredients, addIngredient, removeIngredient, addSection, removeSection, } = useIngredients(); // Instruction handlers const addInstructionHandler = () => { 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 => { const all: Record = {}; for (const ing of ingredients) all[ing.Id] = true; return all; }; const markAllInstructionsDirty = (): Record => { const all: Record = {}; 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; } void createRecipe(); } // EFFECTS useEffect(() => { // Execute validation every time inputs change setValidation( validateCreateRecipeForm( { title, description, prepTime, cookTime, servingSize, category, difficulty, ingredients, instructions }, dirty ) ); }, [title, description, prepTime, cookTime, servingSize, category, difficulty, instructions, ingredients, dirty]); useEffect(() => { // The form is only valid when every item is valid! const bools_valid = Object.values(validation).filter(x => typeof x === "boolean").every(x => x); const ingredients_valid = validation.ingredients.filter(x => !x.valid).length === 0; const instructions_valid = validation.instructions.filter(x => !x.valid).length === 0; setIsFormValid(bools_valid && ingredients_valid && instructions_valid); }, [validation, dirty]); useEffect(() => { console.debug("@validation", validation); }, [validation]); useEffect(() => { console.debug("@dirty", dirty); }, [dirty]); return ( <>

Welcome to the Recipe Creation Wizard! Simply fill in the details about your culinary creation, including the recipe's name, a description, and other specifics like its category, duration, and difficulty. Don't forget to dynamically add all your ingredients and instructions using the dedicated buttons, and feel free to upload an appealing image. All required fields are marked with an *. Once everything looks perfect, just hit the "Create Recipe" button to share your masterpiece!

{/* Title Input */} {/* Description Input */} {/* Tag Input */} {/* Time Input */}
{/* Dropdown Inputs */}
{/* Ingredient Inputs */} {sections.map((section, i) => ( 1} > ))} {/* Instruction Inputs */} <> {/* TODO: Images Input */}

Please provide an image of your creation. This is optional but is a nice touch!

{/* Display the reason for the invalidation */}
); }