diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 13b0804..348d60d 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "dependencies": { "@albedo-link/intent": "^0.13.0", + "@hookform/resolvers": "^5.4.0", "@radix-ui/react-slot": "^1.0.2", "@stellar/freighter-api": "^6.0.1", "@stellar/stellar-sdk": "^14.5.0", @@ -27,6 +28,7 @@ "qrcode": "^1.5.4", "react": "18.2.0", "react-dom": "18.2.0", + "react-hook-form": "^7.80.0", "recharts": "^3.7.0", "tailwind-merge": "^2.0.0", "tailwindcss": "^3.3.0", @@ -2154,6 +2156,18 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@hookform/resolvers": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.4.0.tgz", + "integrity": "sha512-EIsqr/t/qbinPIhGjMdtvutIN1Kk4uwbROE9/UQ93CAVGR7GkA7Y92+fX80OzXi/OB67jVFYwKGO1WzkxmkFZw==", + "license": "MIT", + "dependencies": { + "@standard-schema/utils": "^0.3.0" + }, + "peerDependencies": { + "react-hook-form": "^7.55.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", @@ -12289,6 +12303,22 @@ "react": "^18.2.0" } }, + "node_modules/react-hook-form": { + "version": "7.80.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.80.0.tgz", + "integrity": "sha512-4P+fk6oXsxY+6xSj7Euhc2sumQD8zQqCuVHoJwoyp9EchP+IUW9OESB7uHFJOKsIBQ4MQqYE84INJFqUCYNoOg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index 6a489a4..94fa5de 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,6 +12,7 @@ }, "dependencies": { "@albedo-link/intent": "^0.13.0", + "@hookform/resolvers": "^5.4.0", "@radix-ui/react-slot": "^1.0.2", "@stellar/freighter-api": "^6.0.1", "@stellar/stellar-sdk": "^14.5.0", @@ -30,6 +31,7 @@ "qrcode": "^1.5.4", "react": "18.2.0", "react-dom": "18.2.0", + "react-hook-form": "^7.80.0", "recharts": "^3.7.0", "tailwind-merge": "^2.0.0", "tailwindcss": "^3.3.0", diff --git a/frontend/src/components/auth/LoginForm.tsx b/frontend/src/components/auth/LoginForm.tsx index 4673835..7de7104 100644 --- a/frontend/src/components/auth/LoginForm.tsx +++ b/frontend/src/components/auth/LoginForm.tsx @@ -1,6 +1,7 @@ "use client"; -import { useState, useEffect } from 'react'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; import { useAuth } from '@/hooks/useAuth'; import { useRouter } from 'next/navigation'; import Link from 'next/link'; @@ -9,6 +10,7 @@ import { Input } from '@/components/ui/Input'; import { SocialLogin } from './SocialLogin'; import { AlertCircle } from 'lucide-react'; import { cn } from '@/lib/utils'; +import { loginSchema, type LoginFormInput, type LoginFormData } from '@/lib/validations/auth'; interface LoginFormProps { className?: string; @@ -17,49 +19,25 @@ interface LoginFormProps { export function LoginForm({ className }: LoginFormProps) { const { login, loading, error, clearError } = useAuth(); const router = useRouter(); - - const [formData, setFormData] = useState({ - email: '', - password: '', - rememberMe: false, + + const { + register, + handleSubmit, + formState: { errors }, + } = useForm({ + resolver: zodResolver(loginSchema), + defaultValues: { email: '', password: '', rememberMe: false }, }); - - const [errors, setErrors] = useState>({}); - // Clear errors when form changes - useEffect(() => { + const onSubmit = async (values: LoginFormData) => { clearError(); - setErrors({}); - }, [formData, clearError]); - - const validate = () => { - const newErrors: Record = {}; - - if (!formData.email.trim()) { - newErrors.email = 'Email is required'; - } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) { - newErrors.email = 'Invalid email address'; - } - - if (!formData.password) { - newErrors.password = 'Password is required'; - } - - setErrors(newErrors); - return Object.keys(newErrors).length === 0; - }; - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - - if (!validate()) return; - await login({ - email: formData.email, - password: formData.password, - rememberMe: formData.rememberMe, + email: values.email, + password: values.password, + rememberMe: values.rememberMe, }); - + if (!error) { router.push('/'); } @@ -72,7 +50,7 @@ export function LoginForm({ className }: LoginFormProps) { return (
-
+
@@ -110,8 +87,7 @@ export function LoginForm({ className }: LoginFormProps) { type="password" autoComplete="current-password" placeholder="••••••••" - value={formData.password} - onChange={(e) => setFormData({ ...formData, password: e.target.value })} + {...register('password')} error={!!errors.password || !!error} aria-describedby={errors.password ? "password-error" : undefined} aria-invalid={!!errors.password || !!error} @@ -119,7 +95,7 @@ export function LoginForm({ className }: LoginFormProps) { {errors.password && (

)}
@@ -135,8 +111,7 @@ export function LoginForm({ className }: LoginFormProps) { setFormData({ ...formData, rememberMe: e.target.checked })} + {...register('rememberMe')} className="h-4 w-4 rounded border-border text-primary focus:ring-primary" /> - {errors.agreeToTerms && ( + {termsError && (

- {errors.agreeToTerms} + {termsError}

)} diff --git a/frontend/src/lib/validations/auth.ts b/frontend/src/lib/validations/auth.ts index 091afe5..1830b44 100644 --- a/frontend/src/lib/validations/auth.ts +++ b/frontend/src/lib/validations/auth.ts @@ -28,5 +28,6 @@ export const registerSchema = z path: ["confirmPassword"], }); +export type LoginFormInput = z.input; export type LoginFormData = z.infer; export type RegisterFormData = z.infer;