Compare commits

...

2 Commits

Author SHA1 Message Date
Hayden Hargreaves
d18710e1fc (FEAT): Worked on tags! They seem good, but need a final check.
This was much easier than the previous implementation, gotta love JS/TS (I
hate this shit).
2025-12-02 22:55:32 -07:00
Hayden Hargreaves
58691fd7a1 (FIX): Added error display list.
This looks nice and is a bit more functional! I really need to break the
form up into components...
2025-12-02 22:37:51 -07:00
4 changed files with 98 additions and 32 deletions

View 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>
);
}

View File

@ -1,7 +1,8 @@
import { useEffect, useState } from "react";
import { useEffect, useState, type ChangeEvent, type FormEvent } from "react";
import Banner from "../components/Banner";
import { isRecipeMeal } from "../types/recipe";
import InstructionForm from "../components/forms/InstructionForm";
import ValidationErrorList from "../components/forms/ValidationErrorList";
interface CreateRecipeForm {
title: string;
@ -18,7 +19,7 @@ interface CreateRecipeForm {
image: File | null;
};
interface CreateRecipeFormToggles {
export interface CreateRecipeFormToggles {
title: boolean;
description: boolean;
prepTime: boolean;
@ -103,16 +104,7 @@ export default function Create() {
// HANDLERS
// TODO: Only needed if we use the form element
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 changeHandler = (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
const { name, value } = e.target;
setInputs(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 = () => {
setInstructions([...instructions, { id: crypto.randomUUID(), content: "" }]);
}
// EFFECTS
useEffect(() => {
// Execute validation every time inputs change
validate();
// console.log("@inputs", inputs);
console.log("@inputs", inputs);
}, [inputs, instructions]);
// useEffect(() => {
@ -175,7 +200,6 @@ export default function Create() {
Please provide a unique title for your recipe. This is the most important part!
</p>
<input
onKeyDown={keyDownHandler}
className={`${!validation.title ? "border-red-500" : ""} ${INPUT_CLASSES}`}
type="text"
name="title"
@ -219,7 +243,7 @@ export default function Create() {
</div>
{/* 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">
<label htmlFor="tags" className="text-sm">
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>
</p>
<input
onKeyDown={keyDownHandler}
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"
type="text"
value={inputs.tagInput}
onChange={changeHandler}
name="tagInput"
maxLength={32}
enterKeyHint="done"
type="text"
id="tag"
name="tag"
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="" />
</div>
<ul id="tag-list" className="my-2 flex gap-1 flex-wrap"></ul>
</div>
<ul id="tag-list" className="my-2 flex gap-1 flex-wrap">
{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)}>
&times; {tag}
</button>
</li>
)}
</ul>
</form>
{/* Time Input */}
<div className="my-4 flex gap-x-2">
@ -253,7 +288,6 @@ export default function Create() {
Please provide the estimated prep time (minutes).
</p>
<input
onKeyDown={keyDownHandler}
className={`${!validation.prepTime ? "border-red-500" : ""} ${INPUT_CLASSES}`}
type="number"
name="prepTime"
@ -277,7 +311,6 @@ export default function Create() {
Please provide the estimated cook time (minutes).
</p>
<input
onKeyDown={keyDownHandler}
className={`${!validation.cookTime ? "border-red-500" : ""} ${INPUT_CLASSES}`}
type="number"
name="cookTime"
@ -301,7 +334,6 @@ export default function Create() {
Please provide the estimated serving size.
</p>
<input
onKeyDown={keyDownHandler}
className={`${!validation.servingSize ? "border-red-500" : ""} ${INPUT_CLASSES}`}
type="number"
name="servingSize"
@ -387,7 +419,6 @@ export default function Create() {
<li className="w-full flex gap-x-2 py-2">
<div className="flex-grow">
<input
onKeyDown={keyDownHandler}
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
invalid:border-red-500"
@ -404,7 +435,6 @@ export default function Create() {
</div>
<div className="w-1/3">
<input
onKeyDown={keyDownHandler}
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
invalid:border-red-500"
@ -429,7 +459,6 @@ export default function Create() {
</button>
</div>
{/* TODO: Instructions Inputs */}
<div className="flex flex-col my-4">
<label htmlFor="instructions" className="text-sm">
Instructions
@ -466,7 +495,10 @@ export default function Create() {
/>
</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
</button>
</div>