(FEAT): Worked on the instruction list!
This is great! Looking so much better than it did before! And it works the way I wanted it to! Yayy!!! The reorder stuff is awesome too!
This commit is contained in:
parent
1acc3792c5
commit
728b7eb28c
115
web/package-lock.json
generated
115
web/package-lock.json
generated
@ -8,10 +8,12 @@
|
|||||||
"name": "web",
|
"name": "web",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@tailwindcss/vite": "^4.1.16",
|
"@tailwindcss/vite": "^4.1.16",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
"eslint-plugin-react-dom": "^2.2.4",
|
"eslint-plugin-react-dom": "^2.2.4",
|
||||||
"eslint-plugin-react-x": "^2.2.4",
|
"eslint-plugin-react-x": "^2.2.4",
|
||||||
|
"motion": "^12.23.25",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-cookie": "^8.0.1",
|
"react-cookie": "^8.0.1",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
@ -33,6 +35,45 @@
|
|||||||
"vite": "^7.1.7"
|
"vite": "^7.1.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@dnd-kit/accessibility": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dnd-kit/core": {
|
||||||
|
"version": "6.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
|
||||||
|
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@dnd-kit/accessibility": "^3.1.1",
|
||||||
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0",
|
||||||
|
"react-dom": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dnd-kit/utilities": {
|
||||||
|
"version": "3.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz",
|
||||||
|
"integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@esbuild/aix-ppc64": {
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
"version": "0.25.11",
|
"version": "0.25.11",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz",
|
||||||
@ -2676,6 +2717,33 @@
|
|||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/framer-motion": {
|
||||||
|
"version": "12.23.25",
|
||||||
|
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.25.tgz",
|
||||||
|
"integrity": "sha512-gUHGl2e4VG66jOcH0JHhuJQr6ZNwrET9g31ZG0xdXzT0CznP7fHX4P8Bcvuc4MiUB90ysNnWX2ukHRIggkl6hQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"motion-dom": "^12.23.23",
|
||||||
|
"motion-utils": "^12.23.6",
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@emotion/is-prop-valid": "*",
|
||||||
|
"react": "^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^18.0.0 || ^19.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@emotion/is-prop-valid": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fsevents": {
|
"node_modules/fsevents": {
|
||||||
"version": "2.3.3",
|
"version": "2.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
@ -3332,6 +3400,47 @@
|
|||||||
"node": "*"
|
"node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/motion": {
|
||||||
|
"version": "12.23.25",
|
||||||
|
"resolved": "https://registry.npmjs.org/motion/-/motion-12.23.25.tgz",
|
||||||
|
"integrity": "sha512-Fk5Y1kcgxYiTYOUjmwfXQAP7tP+iGqw/on1UID9WEL/6KpzxPr9jY2169OsjgZvXJdpraKXy0orkjaCVIl5fgQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"framer-motion": "^12.23.25",
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@emotion/is-prop-valid": "*",
|
||||||
|
"react": "^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^18.0.0 || ^19.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@emotion/is-prop-valid": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/motion-dom": {
|
||||||
|
"version": "12.23.23",
|
||||||
|
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.23.tgz",
|
||||||
|
"integrity": "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"motion-utils": "^12.23.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/motion-utils": {
|
||||||
|
"version": "12.23.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz",
|
||||||
|
"integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
@ -3903,6 +4012,12 @@
|
|||||||
"integrity": "sha512-6s5V71mX8qBUmlgbrfL33xDUwO0fq48rxAu2LBE11WBeGdpCPOsXksQbZJHvHwhrd3QjUusd3mAOM5Gg0mFBLg==",
|
"integrity": "sha512-6s5V71mX8qBUmlgbrfL33xDUwO0fq48rxAu2LBE11WBeGdpCPOsXksQbZJHvHwhrd3QjUusd3mAOM5Gg0mFBLg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/tslib": {
|
||||||
|
"version": "2.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||||
|
"license": "0BSD"
|
||||||
|
},
|
||||||
"node_modules/type-check": {
|
"node_modules/type-check": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||||
|
|||||||
@ -10,10 +10,12 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@tailwindcss/vite": "^4.1.16",
|
"@tailwindcss/vite": "^4.1.16",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
"eslint-plugin-react-dom": "^2.2.4",
|
"eslint-plugin-react-dom": "^2.2.4",
|
||||||
"eslint-plugin-react-x": "^2.2.4",
|
"eslint-plugin-react-x": "^2.2.4",
|
||||||
|
"motion": "^12.23.25",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-cookie": "^8.0.1",
|
"react-cookie": "^8.0.1",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
|
|||||||
80
web/src/components/forms/InstructionElement.tsx
Normal file
80
web/src/components/forms/InstructionElement.tsx
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import { useEffect, useState, type ChangeEvent } from "react";
|
||||||
|
import type { Instruction } from "../../pages/Create";
|
||||||
|
import { Reorder, useDragControls } from "motion/react";
|
||||||
|
import DragIconSmall from "../icons/DragIconSmall";
|
||||||
|
|
||||||
|
interface InstructionElementProps {
|
||||||
|
instruction: Instruction;
|
||||||
|
index: number;
|
||||||
|
allowDelete: boolean;
|
||||||
|
onChange: (id: string, value: string) => void;
|
||||||
|
onDelete: (id: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function InstructionElement({ instruction, index, allowDelete, onChange, onDelete }: InstructionElementProps) {
|
||||||
|
const controls = useDragControls();
|
||||||
|
|
||||||
|
const [valid, setValid] = useState<boolean>(true);
|
||||||
|
const [dirty, setDirty] = useState<boolean>(false);
|
||||||
|
|
||||||
|
// HANDLERS
|
||||||
|
const changeHandler = (e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
|
// No need to set many times
|
||||||
|
if (!dirty) setDirty(true);
|
||||||
|
|
||||||
|
onChange(instruction.id, e.target.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// EFFECTS
|
||||||
|
useEffect(() => {
|
||||||
|
if (dirty)
|
||||||
|
setValid(instruction.content !== "");
|
||||||
|
}, [dirty, instruction]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Reorder.Item
|
||||||
|
value={instruction}
|
||||||
|
dragListener={false}
|
||||||
|
dragControls={controls}
|
||||||
|
className="flex items-center"
|
||||||
|
>
|
||||||
|
<div className="flex flex-grow items-center select-none">
|
||||||
|
<h2 className="text-lg md:text-xl mr-4 text-gray-500">{index + 1}.</h2>
|
||||||
|
<div className="flex flex-col flex-grow">
|
||||||
|
<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"
|
||||||
|
name="instructions"
|
||||||
|
value={instruction.content}
|
||||||
|
onChange={changeHandler}
|
||||||
|
rows={3}
|
||||||
|
required
|
||||||
|
minLength={1}
|
||||||
|
placeholder="Describe this step..."
|
||||||
|
/>
|
||||||
|
{!valid && (
|
||||||
|
<p className="text-xs text-red-500 my-1">
|
||||||
|
Please enter an instruction (blank entries are not allowed).
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
<div className="p-2 pr-0 cursor-grab" onPointerDown={e => controls.start(e)}>
|
||||||
|
<DragIconSmall />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
tabIndex={-1}
|
||||||
|
disabled={!allowDelete}
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<svg className="size-6" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M12.0004 9.5L17.0004 14.5M17.0004 9.5L12.0004 14.5M4.50823 13.9546L7.43966 17.7546C7.79218 18.2115 7.96843 18.44 8.18975 18.6047C8.38579 18.7505 8.6069 18.8592 8.84212 18.9253C9.10766 19 9.39623 19 9.97336 19H17.8004C18.9205 19 19.4806 19 19.9084 18.782C20.2847 18.5903 20.5907 18.2843 20.7824 17.908C21.0004 17.4802 21.0004 16.9201 21.0004 15.8V8.2C21.0004 7.0799 21.0004 6.51984 20.7824 6.09202C20.5907 5.71569 20.2847 5.40973 19.9084 5.21799C19.4806 5 18.9205 5 17.8004 5H9.97336C9.39623 5 9.10766 5 8.84212 5.07467C8.6069 5.14081 8.38579 5.2495 8.18975 5.39534C7.96843 5.55998 7.79218 5.78846 7.43966 6.24543L4.50823 10.0454C3.96863 10.7449 3.69883 11.0947 3.59505 11.4804C3.50347 11.8207 3.50347 12.1793 3.59505 12.5196C3.69883 12.9053 3.96863 13.2551 4.50823 13.9546Z" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Reorder.Item>
|
||||||
|
);
|
||||||
|
}
|
||||||
45
web/src/components/forms/InstructionForm.tsx
Normal file
45
web/src/components/forms/InstructionForm.tsx
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { Reorder } from "motion/react";
|
||||||
|
import type { Instruction } from "../../pages/Create";
|
||||||
|
import InstructionElement from "./InstructionElement";
|
||||||
|
|
||||||
|
|
||||||
|
interface InstructionFormProps {
|
||||||
|
instructions: Instruction[];
|
||||||
|
setInstructions: React.Dispatch<React.SetStateAction<Instruction[]>>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function InstructionForm({ instructions, setInstructions }: InstructionFormProps) {
|
||||||
|
const handleChange = (id: string, value: string) => {
|
||||||
|
setInstructions(prev =>
|
||||||
|
prev.map(instr =>
|
||||||
|
instr.id === id ? { ...instr, content: value } : instr
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = (id: string) => {
|
||||||
|
setInstructions(prev =>
|
||||||
|
prev.filter(instr => instr.id !== id)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Reorder.Group
|
||||||
|
axis="y"
|
||||||
|
values={instructions}
|
||||||
|
onReorder={setInstructions}
|
||||||
|
className="flex flex-col gap-2 my-2"
|
||||||
|
>
|
||||||
|
{instructions.map((instruction, i) => (
|
||||||
|
<InstructionElement
|
||||||
|
key={instruction.id}
|
||||||
|
index={i}
|
||||||
|
instruction={instruction}
|
||||||
|
allowDelete={instructions.length > 1}
|
||||||
|
onChange={handleChange}
|
||||||
|
onDelete={handleDelete}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Reorder.Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
7
web/src/components/icons/DragIconSmall.tsx
Normal file
7
web/src/components/icons/DragIconSmall.tsx
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export default function DragIconSmall() {
|
||||||
|
return (
|
||||||
|
<svg className="text-gray-500 size-6" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M4 6H20M4 12H20M4 18H20" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,6 +1,7 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } 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";
|
||||||
|
|
||||||
interface CreateRecipeForm {
|
interface CreateRecipeForm {
|
||||||
title: string;
|
title: string;
|
||||||
@ -13,7 +14,7 @@ interface CreateRecipeForm {
|
|||||||
category: string;
|
category: string;
|
||||||
difficulty: string; // We use this as a number...
|
difficulty: string; // We use this as a number...
|
||||||
ingredients: { name: string; quantity: string }[];
|
ingredients: { name: string; quantity: string }[];
|
||||||
instructions: string[];
|
// Instructions are stored elsewhere
|
||||||
image: File | null;
|
image: File | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -30,6 +31,11 @@ interface CreateRecipeFormToggles {
|
|||||||
// TODO: Image
|
// TODO: Image
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Instruction {
|
||||||
|
id: string;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Classes which are applied to all of the input elements.
|
* Classes which are applied to all of the input elements.
|
||||||
*/
|
*/
|
||||||
@ -48,9 +54,10 @@ export default function Create() {
|
|||||||
category: "",
|
category: "",
|
||||||
difficulty: "",
|
difficulty: "",
|
||||||
ingredients: [{ name: "", quantity: "" }],
|
ingredients: [{ name: "", quantity: "" }],
|
||||||
instructions: [""],
|
|
||||||
image: null,
|
image: null,
|
||||||
});
|
});
|
||||||
|
// Store complex values elsewhere
|
||||||
|
const [instructions, setInstructions] = useState<Instruction[]>([{ id: crypto.randomUUID(), content: "" }, { id: crypto.randomUUID(), content: "" }]);
|
||||||
|
|
||||||
// VALIDATION STATE
|
// VALIDATION STATE
|
||||||
const [validation, setValidation] = useState<CreateRecipeFormToggles>({
|
const [validation, setValidation] = useState<CreateRecipeFormToggles>({
|
||||||
@ -72,8 +79,8 @@ export default function Create() {
|
|||||||
servingSize: false,
|
servingSize: false,
|
||||||
category: false,
|
category: false,
|
||||||
difficulty: false,
|
difficulty: false,
|
||||||
ingredients: false,
|
ingredients: true, // This can be ignored since they're self contained
|
||||||
instructions: false,
|
instructions: true, // This we can ignore since they're self contained
|
||||||
});
|
});
|
||||||
const [isFormValid, setIsFormValid] = useState<boolean>(false);
|
const [isFormValid, setIsFormValid] = useState<boolean>(false);
|
||||||
|
|
||||||
@ -89,6 +96,7 @@ export default function Create() {
|
|||||||
|
|
||||||
state.category = dirty.category ? (inputs.category !== "" && isRecipeMeal(inputs.category)) : 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;
|
state.difficulty = dirty.difficulty ? (inputs.difficulty !== "" && Number(inputs.difficulty) >= 1 && Number(inputs.difficulty) <= 5) : true;
|
||||||
|
state.instructions = instructions?.filter(x => x.content === "").length === 0; // All of them are not empty
|
||||||
|
|
||||||
setValidation(state);
|
setValidation(state);
|
||||||
}
|
}
|
||||||
@ -104,7 +112,7 @@ export default function Create() {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
|
const changeHandler = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
|
||||||
const { name, value } = e.target;
|
const { name, value } = e.target;
|
||||||
setInputs(prev => ({
|
setInputs(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
@ -116,12 +124,25 @@ export default function Create() {
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const addInstructionHandler = () => {
|
||||||
|
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]);
|
}, [inputs, instructions]);
|
||||||
|
|
||||||
|
// useEffect(() => {
|
||||||
|
// console.log("@validation", validation);
|
||||||
|
// }, [validation]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log("@instructions", instructions);
|
||||||
|
}, [instructions]);
|
||||||
|
|
||||||
|
|
||||||
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 has been touched, and every item is valid!
|
||||||
@ -159,7 +180,7 @@ export default function Create() {
|
|||||||
type="text"
|
type="text"
|
||||||
name="title"
|
name="title"
|
||||||
value={inputs.title}
|
value={inputs.title}
|
||||||
onChange={handleChange}
|
onChange={changeHandler}
|
||||||
required
|
required
|
||||||
maxLength={128}
|
maxLength={128}
|
||||||
minLength={1}
|
minLength={1}
|
||||||
@ -183,7 +204,7 @@ export default function Create() {
|
|||||||
className={`${!validation.description ? "border-red-500" : ""} ${INPUT_CLASSES} min-h-32`}
|
className={`${!validation.description ? "border-red-500" : ""} ${INPUT_CLASSES} min-h-32`}
|
||||||
name="description"
|
name="description"
|
||||||
value={inputs.description}
|
value={inputs.description}
|
||||||
onChange={handleChange}
|
onChange={changeHandler}
|
||||||
rows={4}
|
rows={4}
|
||||||
required
|
required
|
||||||
maxLength={1024}
|
maxLength={1024}
|
||||||
@ -237,7 +258,7 @@ export default function Create() {
|
|||||||
type="number"
|
type="number"
|
||||||
name="prepTime"
|
name="prepTime"
|
||||||
value={inputs.prepTime}
|
value={inputs.prepTime}
|
||||||
onChange={handleChange}
|
onChange={changeHandler}
|
||||||
required
|
required
|
||||||
min="0"
|
min="0"
|
||||||
max="120"
|
max="120"
|
||||||
@ -261,7 +282,7 @@ export default function Create() {
|
|||||||
type="number"
|
type="number"
|
||||||
name="cookTime"
|
name="cookTime"
|
||||||
value={inputs.cookTime}
|
value={inputs.cookTime}
|
||||||
onChange={handleChange}
|
onChange={changeHandler}
|
||||||
required
|
required
|
||||||
min="0"
|
min="0"
|
||||||
max="120"
|
max="120"
|
||||||
@ -285,7 +306,7 @@ export default function Create() {
|
|||||||
type="number"
|
type="number"
|
||||||
name="servingSize"
|
name="servingSize"
|
||||||
value={inputs.servingSize}
|
value={inputs.servingSize}
|
||||||
onChange={handleChange}
|
onChange={changeHandler}
|
||||||
max="16"
|
max="16"
|
||||||
min="1"
|
min="1"
|
||||||
required
|
required
|
||||||
@ -311,7 +332,7 @@ export default function Create() {
|
|||||||
className={`${!validation.category ? "border-red-500" : ""} ${INPUT_CLASSES}`}
|
className={`${!validation.category ? "border-red-500" : ""} ${INPUT_CLASSES}`}
|
||||||
name="category"
|
name="category"
|
||||||
value={inputs.category}
|
value={inputs.category}
|
||||||
onChange={handleChange}
|
onChange={changeHandler}
|
||||||
required
|
required
|
||||||
>
|
>
|
||||||
<option value="">Select a category</option>
|
<option value="">Select a category</option>
|
||||||
@ -339,7 +360,7 @@ export default function Create() {
|
|||||||
className={`${!validation.category ? "border-red-500" : ""} ${INPUT_CLASSES}`}
|
className={`${!validation.category ? "border-red-500" : ""} ${INPUT_CLASSES}`}
|
||||||
name="difficulty"
|
name="difficulty"
|
||||||
value={inputs.difficulty}
|
value={inputs.difficulty}
|
||||||
onChange={handleChange}
|
onChange={changeHandler}
|
||||||
required
|
required
|
||||||
>
|
>
|
||||||
<option value="">Select a difficulty</option>
|
<option value="">Select a difficulty</option>
|
||||||
@ -417,25 +438,12 @@ export default function Create() {
|
|||||||
<p className="text-xs py-1 text-gray-700">
|
<p className="text-xs py-1 text-gray-700">
|
||||||
Please provide a list of instructions. You do not need to include step number, they will be added automatically!
|
Please provide a list of instructions. You do not need to include step number, they will be added automatically!
|
||||||
</p>
|
</p>
|
||||||
<div id="instruction-list" className="flex flex-col">
|
|
||||||
<textarea
|
<InstructionForm instructions={instructions} setInstructions={setInstructions} />
|
||||||
className="peer 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 resize-none shadow-sm invalid:border-red-500
|
|
||||||
valid:my-2 invalid:mt-2"
|
|
||||||
id="instructions"
|
|
||||||
name="instructions"
|
|
||||||
rows={3}
|
|
||||||
required
|
|
||||||
minLength={1}
|
|
||||||
placeholder="Step 1: Describe this step..."
|
|
||||||
></textarea>
|
|
||||||
<p className="text-xs text-red-500 my-1">
|
|
||||||
Please enter at least one step.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
onClick={addInstructionHandler}
|
||||||
className="text-base md:text-lg text-white bg-blue-500 w-fit px-5 py-2 rounded-lg cursor-pointer"
|
className="text-sm md:text-base text-white bg-blue-500 w-full md:w-fit px-8 py-2.5 rounded-lg cursor-pointer"
|
||||||
>
|
>
|
||||||
Add Instruction Step
|
Add Instruction Step
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user