Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 4 additions & 15 deletions src/app/(auth)/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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 && (
<p className="mt-1 text-sm text-red-600">{errors.email.message}</p>
)}
<FieldError error={errors.email?.message} id="email-error" />
</div>

<div>
Expand All @@ -123,9 +122,7 @@ export default function LoginPage() {
{showPassword ? <EyeOff size={20} /> : <Eye size={20} />}
</button>
</div>
{errors.password && (
<p className="mt-1 text-sm text-red-600">{errors.password.message}</p>
)}
<FieldError error={errors.password?.message} id="password-error" />
</div>

<div className="flex items-center justify-end">
Expand All @@ -137,15 +134,7 @@ export default function LoginPage() {
</Link>
</div>

{apiError && (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
className="p-3 bg-red-50 border border-red-200 rounded-lg"
>
<p className="text-sm text-red-600">{apiError}</p>
</motion.div>
)}
<FormError error={apiError} id="login-api-error" />

{successMessage && (
<motion.div
Expand Down
23 changes: 5 additions & 18 deletions src/app/(auth)/signup/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Eye, EyeOff } from 'lucide-react';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { signupSchema, SignupFormData } from '../../lib/validationSchemas';
import { FormError, FieldError } from '../../../components/forms/FormError';

export default function SignupPage() {
const [isLoading, setIsLoading] = useState(false);
Expand Down Expand Up @@ -99,9 +100,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('name')}
/>
{errors.name && (
<p className="mt-1 text-sm text-red-600">{errors.name.message}</p>
)}
<FieldError error={errors.name?.message} id="name-error" />
</div>

<div>
Expand All @@ -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 && (
<p className="mt-1 text-sm text-red-600">{errors.email.message}</p>
)}
<FieldError error={errors.email?.message} id="email-error" />
</div>

<div>
Expand All @@ -138,20 +135,10 @@ export default function SignupPage() {
{showPassword ? <EyeOff size={20} /> : <Eye size={20} />}
</button>
</div>
{errors.password && (
<p className="mt-1 text-sm text-red-600">{errors.password.message}</p>
)}
<FieldError error={errors.password?.message} id="password-error" />
</div>

{apiError && (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
className="p-3 bg-red-50 border border-red-200 rounded-lg"
>
<p className="text-sm text-red-600">{apiError}</p>
</motion.div>
)}
<FormError error={apiError} id="signup-api-error" />

{successMessage && (
<motion.div
Expand Down
11 changes: 2 additions & 9 deletions src/app/components/auth/FormInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { motion } from 'framer-motion';
import { Eye, EyeOff } from 'lucide-react';
import { useState, InputHTMLAttributes, forwardRef } from 'react';
import { FieldError } from '../../../components/forms/FormError';

interface FormInputProps extends InputHTMLAttributes<HTMLInputElement> {
label: string;
Expand Down Expand Up @@ -69,15 +70,7 @@ export const FormInput = forwardRef<HTMLInputElement, FormInputProps>(
</button>
)}
</div>
{error && (
<motion.p
initial={{ opacity: 0, y: -5 }}
animate={{ opacity: 1, y: 0 }}
className="mt-1 text-sm text-red-500"
>
{error}
</motion.p>
)}
<FieldError error={error} />
</motion.div>
);
}
Expand Down
21 changes: 6 additions & 15 deletions src/app/components/profile/ProfileEditForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down Expand Up @@ -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 && (
<p className="mt-1 text-sm text-red-600">{errors.firstName.message}</p>
)}
<FieldError error={errors.firstName?.message} id="firstName-error" />
</div>

<div>
Expand All @@ -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 && (
<p className="mt-1 text-sm text-red-600">{errors.lastName.message}</p>
)}
<FieldError error={errors.lastName?.message} id="lastName-error" />
</div>
</div>

Expand All @@ -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 && (
<p className="mt-1 text-sm text-red-600">{errors.email.message}</p>
)}
<FieldError error={errors.email?.message} id="profile-email-error" />
</div>

<div className="mt-6">
Expand All @@ -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 && (
<p className="mt-1 text-sm text-red-600">{errors.bio.message}</p>
)}
<FieldError error={errors.bio?.message} id="profile-bio-error" />
</div>

<div className="mt-6">
Expand Down Expand Up @@ -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 && (
<p className="mt-1 text-sm text-red-600">{errors.website.message}</p>
)}
<FieldError error={errors.website?.message} id="website-error" />
</div>

<div>
Expand Down
71 changes: 71 additions & 0 deletions src/components/forms/FormError.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLDivElement>(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 (
<motion.div
ref={errorRef}
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
className={`p-3 bg-red-50 border border-red-200 rounded-lg flex items-start gap-2 ${className}`}
role="alert"
aria-live="assertive"
id={id}
>
<AlertCircle className="w-5 h-5 text-red-500 shrink-0 mt-0.5" />
<div className="flex flex-col">
{errors.map((err, index) => (
<span key={index} className="text-sm text-red-600 font-medium">
{err}
</span>
))}
</div>
</motion.div>
);
}

// --- INLINE FIELD ERROR (For Client Form Validation) ---
interface FieldErrorProps {
error?: string;
id?: string;
}

export function FieldError({ error, id }: FieldErrorProps) {
if (!error) return null;

return (
<motion.p
initial={{ opacity: 0, y: -5 }}
animate={{ opacity: 1, y: 0 }}
className="mt-1.5 text-sm text-red-600 font-medium ml-1"
role="alert"
aria-live="polite"
id={id}
>
{error}
</motion.p>
);
}
Loading