Skip to content
Open
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
30 changes: 30 additions & 0 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
69 changes: 22 additions & 47 deletions frontend/src/components/auth/LoginForm.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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;
Expand All @@ -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<LoginFormInput, unknown, LoginFormData>({
resolver: zodResolver(loginSchema),
defaultValues: { email: '', password: '', rememberMe: false },
});

const [errors, setErrors] = useState<Record<string, string>>({});

// Clear errors when form changes
useEffect(() => {
const onSubmit = async (values: LoginFormData) => {
clearError();
setErrors({});
}, [formData, clearError]);

const validate = () => {
const newErrors: Record<string, string> = {};

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('/');
}
Expand All @@ -72,7 +50,7 @@ export function LoginForm({ className }: LoginFormProps) {

return (
<div className={cn('space-y-6', className)}>
<form onSubmit={handleSubmit} className="space-y-4">
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<div className="space-y-1">
<label htmlFor="email" className="block text-sm font-medium text-foreground">
Email address
Expand All @@ -82,16 +60,15 @@ export function LoginForm({ className }: LoginFormProps) {
type="email"
autoComplete="email"
placeholder="you@example.com"
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
{...register('email')}
error={!!errors.email || !!error}
aria-describedby={errors.email ? "email-error" : undefined}
aria-invalid={!!errors.email || !!error}
/>
{errors.email && (
<p id="email-error" className="flex items-center gap-1 text-xs text-destructive">
<AlertCircle className="h-3 w-3" aria-hidden="true" />
{errors.email}
{errors.email.message}
</p>
)}
</div>
Expand All @@ -110,16 +87,15 @@ 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}
/>
{errors.password && (
<p id="password-error" className="flex items-center gap-1 text-xs text-destructive">
<AlertCircle className="h-3 w-3" aria-hidden="true" />
{errors.password}
{errors.password.message}
</p>
)}
</div>
Expand All @@ -135,8 +111,7 @@ export function LoginForm({ className }: LoginFormProps) {
<input
id="remember-me"
type="checkbox"
checked={formData.rememberMe}
onChange={(e) => setFormData({ ...formData, rememberMe: e.target.checked })}
{...register('rememberMe')}
className="h-4 w-4 rounded border-border text-primary focus:ring-primary"
/>
<label htmlFor="remember-me" className="ml-2 block text-sm text-muted-foreground">
Expand Down
Loading