From 0db64bde5efc2643fb6e881da8fc3741cfce06b6 Mon Sep 17 00:00:00 2001 From: AbuJulaybeeb Date: Wed, 25 Feb 2026 17:14:23 +0100 Subject: [PATCH] refactor: standardize form validation errors across app Adds reusable FormError and FieldError components with accessibility (aria-live) and auto-scroll functionality. Integrates into auth and profile forms to resolve #77. --- src/app/(auth)/login/page.tsx | 19 ++--- src/app/(auth)/signup/page.tsx | 23 ++---- src/app/components/auth/FormInput.tsx | 11 +-- .../components/profile/ProfileEditForm.tsx | 21 ++---- src/components/forms/FormError.tsx | 71 +++++++++++++++++++ 5 files changed, 88 insertions(+), 57 deletions(-) create mode 100644 src/components/forms/FormError.tsx diff --git a/src/app/(auth)/login/page.tsx b/src/app/(auth)/login/page.tsx index fa1fca2..c5c409a 100644 --- a/src/app/(auth)/login/page.tsx +++ b/src/app/(auth)/login/page.tsx @@ -8,6 +8,7 @@ import { Eye, EyeOff } from 'lucide-react'; import Link from 'next/link'; import { useRouter } from 'next/navigation'; import { loginSchema, LoginFormData } from '../../lib/validationSchemas'; +import { FormError, FieldError } from '../../../components/forms/FormError'; export default function LoginPage() { const [isLoading, setIsLoading] = useState(false); @@ -99,9 +100,7 @@ export default function LoginPage() { className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-cyan-500 focus:border-transparent outline-none transition-all" {...register('email')} /> - {errors.email && ( -

{errors.email.message}

- )} +
@@ -123,9 +122,7 @@ export default function LoginPage() { {showPassword ? : }
- {errors.password && ( -

{errors.password.message}

- )} +
@@ -137,15 +134,7 @@ export default function LoginPage() {
- {apiError && ( - -

{apiError}

-
- )} + {successMessage && ( - {errors.name && ( -

{errors.name.message}

- )} +
@@ -114,9 +113,7 @@ export default function SignupPage() { className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-cyan-500 focus:border-transparent outline-none transition-all" {...register('email')} /> - {errors.email && ( -

{errors.email.message}

- )} +
@@ -138,20 +135,10 @@ export default function SignupPage() { {showPassword ? : }
- {errors.password && ( -

{errors.password.message}

- )} + - {apiError && ( - -

{apiError}

-
- )} + {successMessage && ( { label: string; @@ -69,15 +70,7 @@ export const FormInput = forwardRef( )} - {error && ( - - {error} - - )} + ); } diff --git a/src/app/components/profile/ProfileEditForm.tsx b/src/app/components/profile/ProfileEditForm.tsx index a80065d..fcb9ca1 100644 --- a/src/app/components/profile/ProfileEditForm.tsx +++ b/src/app/components/profile/ProfileEditForm.tsx @@ -7,6 +7,7 @@ import toast from 'react-hot-toast'; import ImageUploader from '@/components/shared/ImageUploader'; import PreferencesSection from '@/components/profile/PreferencesSection'; import { useProfileUpdate } from '@/app/hooks/useProfileUpdate'; +import { FieldError } from '@/components/forms/FormError'; const profileSchema = z.object({ firstName: z.string().min(2, 'First name must be at least 2 characters'), @@ -77,9 +78,7 @@ export default function ProfileEditForm() { {...register('firstName')} className="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" /> - {errors.firstName && ( -

{errors.firstName.message}

- )} +
@@ -91,9 +90,7 @@ export default function ProfileEditForm() { {...register('lastName')} className="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" /> - {errors.lastName && ( -

{errors.lastName.message}

- )} +
@@ -106,9 +103,7 @@ export default function ProfileEditForm() { {...register('email')} className="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" /> - {errors.email && ( -

{errors.email.message}

- )} +
@@ -120,9 +115,7 @@ export default function ProfileEditForm() { rows={4} className="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" /> - {errors.bio && ( -

{errors.bio.message}

- )} +
@@ -150,9 +143,7 @@ export default function ProfileEditForm() { {...register('website')} className="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" /> - {errors.website && ( -

{errors.website.message}

- )} +
diff --git a/src/components/forms/FormError.tsx b/src/components/forms/FormError.tsx new file mode 100644 index 0000000..8715a21 --- /dev/null +++ b/src/components/forms/FormError.tsx @@ -0,0 +1,71 @@ +'use client'; + +import { motion } from 'framer-motion'; +import { AlertCircle } from 'lucide-react'; +import { useEffect, useRef } from 'react'; + +// --- GLOBAL FORM ERROR (For Backend / API Errors) --- +interface FormErrorProps { + error?: string | string[] | null; + className?: string; + id?: string; +} + +export function FormError({ error, className = '', id }: FormErrorProps) { + const errorRef = useRef(null); + + useEffect(() => { + if (error && errorRef.current) { + errorRef.current.scrollIntoView({ behavior: 'smooth', block: 'center' }); + } + }, [error]); + + if (!error) return null; + + const errors = Array.isArray(error) ? error : [error]; + + return ( + + +
+ {errors.map((err, index) => ( + + {err} + + ))} +
+
+ ); +} + +// --- INLINE FIELD ERROR (For Client Form Validation) --- +interface FieldErrorProps { + error?: string; + id?: string; +} + +export function FieldError({ error, id }: FieldErrorProps) { + if (!error) return null; + + return ( + + {error} + + ); +} \ No newline at end of file