diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 484cbbe6..5582e4c4 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,17 +8,20 @@ "name": "frontend", "version": "0.0.0", "dependencies": { + "@hookform/resolvers": "^3.3.2", "axios": "^1.6.1", "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", "lucide-react": "^0.292.0", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-hook-form": "^7.49.2", "react-router-dom": "^6.18.0", "react-tag-input": "^6.8.1", "react-toastify": "^9.1.3", "tailwind-merge": "^2.0.0", - "tailwindcss-animate": "^1.0.7" + "tailwindcss-animate": "^1.0.7", + "zod": "^3.22.4" }, "devDependencies": { "@types/node": "^20.9.0", @@ -479,6 +482,14 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@hookform/resolvers": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.3.2.tgz", + "integrity": "sha512-Tw+GGPnBp+5DOsSg4ek3LCPgkBOuOgS5DsDV7qsWNH9LZc433kgsWICjlsh2J9p04H2K66hsXPPb9qn9ILdUtA==", + "peerDependencies": { + "react-hook-form": "^7.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.13", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", @@ -2910,6 +2921,22 @@ "react": "^18.2.0" } }, + "node_modules/react-hook-form": { + "version": "7.49.2", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.49.2.tgz", + "integrity": "sha512-TZcnSc17+LPPVpMRIDNVITY6w20deMdNi6iehTFLV1x8SqThXGwu93HjlUVU09pzFgZH7qZOvLMM7UYf2ShAHA==", + "engines": { + "node": ">=18", + "pnpm": "8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -3549,6 +3576,14 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.22.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", + "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/frontend/package.json b/frontend/package.json index d4a74c34..dadfdcf9 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,17 +11,20 @@ "format": "prettier --write ." }, "dependencies": { + "@hookform/resolvers": "^3.3.2", "axios": "^1.6.1", "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", "lucide-react": "^0.292.0", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-hook-form": "^7.49.2", "react-router-dom": "^6.18.0", "react-tag-input": "^6.8.1", "react-toastify": "^9.1.3", "tailwind-merge": "^2.0.0", - "tailwindcss-animate": "^1.0.7" + "tailwindcss-animate": "^1.0.7", + "zod": "^3.22.4" }, "devDependencies": { "@types/node": "^20.9.0", diff --git a/frontend/src/lib/blog.zod.ts b/frontend/src/lib/blog.zod.ts new file mode 100644 index 00000000..bd8e5881 --- /dev/null +++ b/frontend/src/lib/blog.zod.ts @@ -0,0 +1,31 @@ +import { z } from 'zod'; + +// validate the image link +const isValidImageLink = (value: string) => { + const imageLinkRegex = /\.(jpg|jpeg|png|webp)$/i; + return imageLinkRegex.test(value); +}; + +export const FormDataSchema = z.object({ + title: z.string().refine((value) => value.trim().split(/\s+/).length >= 3, { + message: 'Oops! Title needs more spice. Give it at least 3 words.', + }), + authorName: z + .string() + .refine((value) => value.length >= 3, { + message: "C'ome on! Your name cannot be less than 3 characters.", + }) + .refine((value) => value.length <= 15, { + message: "Hey isn't it too big of a name, can you limit it to 15 characters", + }), + imageLink: z.string().refine((value) => isValidImageLink(value), { + message: 'Hmm... Image link should end with .jpg, .jpeg, .webp, or .png.', + }), + categories: z.array(z.string()).refine((value) => value.length <= 3, { + message: 'Easy there! Select up to 3 categories.', + }), + description: z.string(), + isFeaturedPost: z.boolean(), +}); + +export type TFormData = z.infer; diff --git a/frontend/src/pages/add-blog.tsx b/frontend/src/pages/add-blog.tsx index 0dc64d95..237ce378 100644 --- a/frontend/src/pages/add-blog.tsx +++ b/frontend/src/pages/add-blog.tsx @@ -1,5 +1,6 @@ +import { useForm } from 'react-hook-form'; import axios from 'axios'; -import { ChangeEvent, FormEvent, useState, useEffect } from 'react'; +import { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { toast } from 'react-toastify'; import 'react-toastify/dist/ReactToastify.css'; @@ -9,99 +10,66 @@ import ModalComponent from '@/components/modal'; import CategoryPill from '@/components/category-pill'; import { categories } from '@/utils/category-colors'; -type FormData = { - title: string; - authorName: string; - imageLink: string; - categories: string[]; - description: string; - isFeaturedPost: boolean; -}; -function AddBlog() { - const [selectedImage, setSelectedImage] = useState(''); +import { zodResolver } from '@hookform/resolvers/zod'; - const handleImageSelect = (imageUrl: string) => { - setSelectedImage(imageUrl); - }; +import { FormDataSchema, TFormData } from '@/lib/blog.zod'; +function AddBlog() { + const [selectedImage, setSelectedImage] = useState(''); const [modal, setmodal] = useState(false); - const [formData, setFormData] = useState({ - title: '', - authorName: '', - imageLink: '', - categories: [], - description: '', - isFeaturedPost: false, + + const { + register, + handleSubmit, + getValues, + setValue, + formState: { errors, isValid }, + } = useForm({ + resolver: zodResolver(FormDataSchema), }); - const handleInputChange = (e: ChangeEvent) => { - const { name, value } = e.target; - setFormData({ ...formData, [name]: value }); + const handleImageSelect = (imageUrl: string) => { + setSelectedImage(imageUrl); + setValue('imageLink', imageUrl); }; const handleCategoryClick = (category: string) => { - if (formData.categories.includes(category)) { - setFormData({ - ...formData, - categories: formData.categories.filter((cat) => cat !== category), - }); + const currentCategories = getValues('categories') || []; + if (currentCategories.includes(category)) { + setValue( + 'categories', + currentCategories.filter((cat: string) => cat !== category) + ); } else { - setFormData({ - ...formData, - categories: [...formData.categories, category], - }); + setValue('categories', [...currentCategories, category]); } }; + const handleselector = () => { - setFormData({ - ...formData, - imageLink: selectedImage, - }); + setValue('imageLink', selectedImage); setmodal(false); }; - const handleCheckboxChange = () => { - setFormData({ ...formData, isFeaturedPost: !formData.isFeaturedPost }); - }; - const validateFormData = () => { - if ( - !formData.title || - !formData.authorName || - !formData.imageLink || - !formData.description || - formData.categories.length === 0 - ) { - toast.error('All fields must be filled out.'); - return false; - } - const imageLinkRegex = /\.(jpg|jpeg|png|webp)$/i; - if (!imageLinkRegex.test(formData.imageLink)) { - toast.error('Image URL must end with .jpg, .jpeg, .webp or .png'); - return false; - } - if (formData.categories.length > 3) { - toast.error('Select up to three categories.'); - return false; - } - return true; - }; - const handleSubmit = async (e: FormEvent) => { - e.preventDefault(); - if (validateFormData()) { + const formSubmit = async (formData: TFormData) => { + console.log('error', errors); + + console.log('From adad', formData); + + if (isValid) { try { const response = await axios.post(import.meta.env.VITE_API_PATH + '/api/posts/', formData); - if (response.status === 200) { toast.success('Blog post successfully created!'); navigate('/'); } else { toast.error('Error: ' + response.data.message); } - } catch (err: any) { - toast.error('Error: ' + err.message); + } catch (error: any) { + toast.error('Error: ' + error.message); } } }; + const navigate = useNavigate(); const [isDarkMode, setIsDarkMode] = useState( @@ -118,7 +86,7 @@ function AddBlog() { mediaQuery.addListener(handleThemeChange); return () => { - mediaQuery.removeListener(handleThemeChange); + -mediaQuery.removeListener(handleThemeChange); }; }, []); @@ -143,18 +111,17 @@ function AddBlog() {
-
+
@@ -164,14 +131,14 @@ function AddBlog() { Blog title
+ {errors.title ?

{`${errors.title.message}`}

: null}
@@ -179,12 +146,11 @@ function AddBlog() { Blog content