Compare commits
2 Commits
728b7eb28c
...
d18710e1fc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d18710e1fc | ||
|
|
58691fd7a1 |
@ -64,8 +64,8 @@ export default function InstructionElement({ instruction, index, allowDelete, on
|
|||||||
<DragIconSmall />
|
<DragIconSmall />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
disabled={!allowDelete}
|
disabled={!allowDelete}
|
||||||
onClick={() => onDelete(instruction.id)}
|
onClick={() => onDelete(instruction.id)}
|
||||||
className="p-2 pr-0 cursor-pointer text-gray-500 hover:text-red-500 disabled:text-gray-200 disabled:cursor-not-allowed duration-300"
|
className="p-2 pr-0 cursor-pointer text-gray-500 hover:text-red-500 disabled:text-gray-200 disabled:cursor-not-allowed duration-300"
|
||||||
|
|||||||
@ -18,7 +18,7 @@ export default function InstructionForm({ instructions, setInstructions }: Instr
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = (id: string) => {
|
const handleDelete = (id: string) => {
|
||||||
setInstructions(prev =>
|
setInstructions(prev =>
|
||||||
prev.filter(instr => instr.id !== id)
|
prev.filter(instr => instr.id !== id)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
34
web/src/components/forms/ValidationErrorList.tsx
Normal file
34
web/src/components/forms/ValidationErrorList.tsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import type { CreateRecipeFormToggles } from "../../pages/Create";
|
||||||
|
|
||||||
|
interface ValidationErrorListProps {
|
||||||
|
validation: CreateRecipeFormToggles;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MESSAGES: Record<keyof CreateRecipeFormToggles, string> = {
|
||||||
|
title: "Invalid title provided.",
|
||||||
|
description: "Invalid description provided.",
|
||||||
|
prepTime: "Invalid preparation time provided.",
|
||||||
|
cookTime: "Invalid cook time provided.",
|
||||||
|
servingSize: "Invalid serving size provided.",
|
||||||
|
category: "Invalid category selected.",
|
||||||
|
difficulty: "Invalid difficulty selected.",
|
||||||
|
ingredients: "Invalid ingredients provided.",
|
||||||
|
instructions: "Invalid instructions provided.",
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ValidationErrorList({ validation }: ValidationErrorListProps) {
|
||||||
|
return (
|
||||||
|
<div className="my-2">
|
||||||
|
{Object.entries(validation)
|
||||||
|
.filter(([, isValid]) => !isValid)
|
||||||
|
.map(([name]) => {
|
||||||
|
const key = name as keyof CreateRecipeFormToggles;
|
||||||
|
return (
|
||||||
|
<p key={name} className="text-sm text-red-500">
|
||||||
|
{MESSAGES[key]}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,7 +1,8 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState, type ChangeEvent, type FormEvent } from "react";
|
||||||
import Banner from "../components/Banner";
|
import Banner from "../components/Banner";
|
||||||
import { isRecipeMeal } from "../types/recipe";
|
import { isRecipeMeal } from "../types/recipe";
|
||||||
import InstructionForm from "../components/forms/InstructionForm";
|
import InstructionForm from "../components/forms/InstructionForm";
|
||||||
|
import ValidationErrorList from "../components/forms/ValidationErrorList";
|
||||||
|
|
||||||
interface CreateRecipeForm {
|
interface CreateRecipeForm {
|
||||||
title: string;
|
title: string;
|
||||||
@ -18,7 +19,7 @@ interface CreateRecipeForm {
|
|||||||
image: File | null;
|
image: File | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface CreateRecipeFormToggles {
|
export interface CreateRecipeFormToggles {
|
||||||
title: boolean;
|
title: boolean;
|
||||||
description: boolean;
|
description: boolean;
|
||||||
prepTime: boolean;
|
prepTime: boolean;
|
||||||
@ -103,16 +104,7 @@ export default function Create() {
|
|||||||
|
|
||||||
|
|
||||||
// HANDLERS
|
// HANDLERS
|
||||||
// TODO: Only needed if we use the form element
|
const changeHandler = (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
|
||||||
const keyDownHandler = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
e.preventDefault();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const changeHandler = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
|
|
||||||
const { name, value } = e.target;
|
const { name, value } = e.target;
|
||||||
setInputs(prev => ({
|
setInputs(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
@ -124,15 +116,48 @@ export default function Create() {
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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: "" }]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// EFFECTS
|
// EFFECTS
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Execute validation every time inputs change
|
// Execute validation every time inputs change
|
||||||
validate();
|
validate();
|
||||||
// console.log("@inputs", inputs);
|
console.log("@inputs", inputs);
|
||||||
}, [inputs, instructions]);
|
}, [inputs, instructions]);
|
||||||
|
|
||||||
// useEffect(() => {
|
// useEffect(() => {
|
||||||
@ -175,7 +200,6 @@ export default function Create() {
|
|||||||
Please provide a unique title for your recipe. This is the most important part!
|
Please provide a unique title for your recipe. This is the most important part!
|
||||||
</p>
|
</p>
|
||||||
<input
|
<input
|
||||||
onKeyDown={keyDownHandler}
|
|
||||||
className={`${!validation.title ? "border-red-500" : ""} ${INPUT_CLASSES}`}
|
className={`${!validation.title ? "border-red-500" : ""} ${INPUT_CLASSES}`}
|
||||||
type="text"
|
type="text"
|
||||||
name="title"
|
name="title"
|
||||||
@ -219,7 +243,7 @@ export default function Create() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* TODO: Tags Input */}
|
{/* TODO: Tags Input */}
|
||||||
<div className="my-4 flex flex-col gap-x-2">
|
<form onSubmit={tagCreationHandler} className="my-4 flex flex-col gap-x-2">
|
||||||
<div className="flex flex-col flex-grow">
|
<div className="flex flex-col flex-grow">
|
||||||
<label htmlFor="tags" className="text-sm">
|
<label htmlFor="tags" className="text-sm">
|
||||||
Recipe Tags
|
Recipe Tags
|
||||||
@ -228,19 +252,30 @@ export default function Create() {
|
|||||||
Please provide a list of tags. <span className="italic">e.g., easy, dairy-free, gluten-free, high protein.</span>
|
Please provide a list of tags. <span className="italic">e.g., easy, dairy-free, gluten-free, high protein.</span>
|
||||||
</p>
|
</p>
|
||||||
<input
|
<input
|
||||||
onKeyDown={keyDownHandler}
|
type="text"
|
||||||
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"
|
value={inputs.tagInput}
|
||||||
|
onChange={changeHandler}
|
||||||
|
name="tagInput"
|
||||||
maxLength={32}
|
maxLength={32}
|
||||||
enterKeyHint="done"
|
enterKeyHint="done"
|
||||||
type="text"
|
|
||||||
id="tag"
|
|
||||||
name="tag"
|
|
||||||
placeholder="e.g., Healthy"
|
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="" />
|
<input type="hidden" name="tags" id="tags" value="" />
|
||||||
</div>
|
</div>
|
||||||
<ul id="tag-list" className="my-2 flex gap-1 flex-wrap"></ul>
|
<ul id="tag-list" className="my-2 flex gap-1 flex-wrap">
|
||||||
</div>
|
{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)}>
|
||||||
|
× {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">
|
||||||
@ -253,7 +288,6 @@ export default function Create() {
|
|||||||
Please provide the estimated prep time (minutes).
|
Please provide the estimated prep time (minutes).
|
||||||
</p>
|
</p>
|
||||||
<input
|
<input
|
||||||
onKeyDown={keyDownHandler}
|
|
||||||
className={`${!validation.prepTime ? "border-red-500" : ""} ${INPUT_CLASSES}`}
|
className={`${!validation.prepTime ? "border-red-500" : ""} ${INPUT_CLASSES}`}
|
||||||
type="number"
|
type="number"
|
||||||
name="prepTime"
|
name="prepTime"
|
||||||
@ -277,7 +311,6 @@ export default function Create() {
|
|||||||
Please provide the estimated cook time (minutes).
|
Please provide the estimated cook time (minutes).
|
||||||
</p>
|
</p>
|
||||||
<input
|
<input
|
||||||
onKeyDown={keyDownHandler}
|
|
||||||
className={`${!validation.cookTime ? "border-red-500" : ""} ${INPUT_CLASSES}`}
|
className={`${!validation.cookTime ? "border-red-500" : ""} ${INPUT_CLASSES}`}
|
||||||
type="number"
|
type="number"
|
||||||
name="cookTime"
|
name="cookTime"
|
||||||
@ -301,7 +334,6 @@ export default function Create() {
|
|||||||
Please provide the estimated serving size.
|
Please provide the estimated serving size.
|
||||||
</p>
|
</p>
|
||||||
<input
|
<input
|
||||||
onKeyDown={keyDownHandler}
|
|
||||||
className={`${!validation.servingSize ? "border-red-500" : ""} ${INPUT_CLASSES}`}
|
className={`${!validation.servingSize ? "border-red-500" : ""} ${INPUT_CLASSES}`}
|
||||||
type="number"
|
type="number"
|
||||||
name="servingSize"
|
name="servingSize"
|
||||||
@ -387,7 +419,6 @@ export default function Create() {
|
|||||||
<li className="w-full flex gap-x-2 py-2">
|
<li className="w-full flex gap-x-2 py-2">
|
||||||
<div className="flex-grow">
|
<div className="flex-grow">
|
||||||
<input
|
<input
|
||||||
onKeyDown={keyDownHandler}
|
|
||||||
className="peer w-full border border-gray-300 px-4 py-2 rounded-lg focus:outline-none
|
className="peer w-full 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
|
focus:ring-blue-500 focus:ring-2 duration-200 ease-in-out transition-all shadow-sm
|
||||||
invalid:border-red-500"
|
invalid:border-red-500"
|
||||||
@ -404,7 +435,6 @@ export default function Create() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="w-1/3">
|
<div className="w-1/3">
|
||||||
<input
|
<input
|
||||||
onKeyDown={keyDownHandler}
|
|
||||||
className="peer w-full border border-gray-300 px-4 py-2 rounded-lg focus:outline-none
|
className="peer w-full 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
|
focus:ring-blue-500 focus:ring-2 duration-200 ease-in-out transition-all shadow-sm
|
||||||
invalid:border-red-500"
|
invalid:border-red-500"
|
||||||
@ -429,7 +459,6 @@ export default function Create() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* TODO: Instructions Inputs */}
|
|
||||||
<div className="flex flex-col my-4">
|
<div className="flex flex-col my-4">
|
||||||
<label htmlFor="instructions" className="text-sm">
|
<label htmlFor="instructions" className="text-sm">
|
||||||
Instructions
|
Instructions
|
||||||
@ -466,7 +495,10 @@ export default function Create() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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 mt-8 py-2 rounded-lg text-lg shadow-md`}>
|
{/* Display the reason for the invalidation */}
|
||||||
|
<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`}>
|
||||||
Create Recipe
|
Create Recipe
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user