mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 21:20:57 +00:00
166 lines
6.6 KiB
TypeScript
166 lines
6.6 KiB
TypeScript
'use client';
|
|
|
|
import { useForm } from 'react-hook-form';
|
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
import { z } from 'zod';
|
|
import { useRouter, useSearchParams } from 'next/navigation';
|
|
import { useAuthStore } from '@/lib/auth-store';
|
|
import { useState, Suspense } from 'react';
|
|
import Link from 'next/link';
|
|
import { Train, ShieldCheck, Eye, EyeOff } from 'lucide-react';
|
|
|
|
const loginSchema = z.object({
|
|
// Accepts either an email or a phone number. Passengers who registered without an
|
|
// email sign in with their phone number, which is sent in the same `email` field —
|
|
// the IAM matches on either identifier.
|
|
email: z.string().min(1, 'Phone or email is required'),
|
|
password: z.string().min(6, 'Password must be at least 6 characters'),
|
|
});
|
|
|
|
type LoginForm = z.infer<typeof loginSchema>;
|
|
|
|
function LoginContent() {
|
|
const router = useRouter();
|
|
const searchParams = useSearchParams();
|
|
const login = useAuthStore((s) => s.login);
|
|
const [error, setError] = useState('');
|
|
const [loading, setLoading] = useState(false);
|
|
const [showPassword, setShowPassword] = useState(false);
|
|
|
|
const { register, handleSubmit, formState: { errors } } = useForm<LoginForm>({
|
|
resolver: zodResolver(loginSchema as any),
|
|
});
|
|
|
|
const onSubmit = async (data: LoginForm) => {
|
|
setLoading(true);
|
|
setError('');
|
|
try {
|
|
await login(data.email, data.password);
|
|
const redirect = searchParams.get('redirect') || '/booking/search';
|
|
router.push(redirect);
|
|
} catch (err: any) {
|
|
setError(err.response?.data?.message || 'Login failed. Please check your credentials.');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gradient-to-br from-[rgb(20_113_76)] from-10% via-transparent to-[rgb(20_113_76)] to-90% dark:from-gray-900 dark:to-gray-800 flex items-center justify-center py-12 px-4">
|
|
<div className="max-w-md w-full">
|
|
<div className="text-center mb-8">
|
|
<div className="flex justify-center mb-4">
|
|
<div className="w-12 h-12 bg-[rgb(20_113_76)] rounded-lg flex items-center justify-center">
|
|
<Train className="w-6 h-6 text-white" />
|
|
</div>
|
|
</div>
|
|
<h1 className="text-3xl font-bold text-gray-900 dark:text-gray-100">Sign in</h1>
|
|
<p className="text-gray-600 dark:text-gray-400 mt-2">Welcome back</p>
|
|
</div>
|
|
|
|
<div className="card">
|
|
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
|
{error && (
|
|
<div className="bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-800 text-red-700 dark:text-red-300 px-4 py-3 rounded">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">Phone or email</label>
|
|
<input
|
|
type="text"
|
|
{...register('email')}
|
|
className="input-field"
|
|
placeholder="+251912345678 or your@email.com"
|
|
autoComplete="username"
|
|
/>
|
|
{errors.email && (
|
|
<p className="text-red-500 text-sm mt-1">{errors.email.message}</p>
|
|
)}
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">Password</label>
|
|
<div className="relative">
|
|
<input
|
|
type={showPassword ? 'text' : 'password'}
|
|
{...register('password')}
|
|
className="input-field pr-10"
|
|
placeholder="••••••••"
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowPassword((v) => !v)}
|
|
className="absolute inset-y-0 right-0 flex items-center pr-3 text-gray-400 hover:text-gray-600 dark:hover:text-gray-200"
|
|
aria-label={showPassword ? 'Hide password' : 'Show password'}
|
|
tabIndex={-1}
|
|
>
|
|
{showPassword ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
|
|
</button>
|
|
</div>
|
|
{errors.password && (
|
|
<p className="text-red-500 text-sm mt-1">{errors.password.message}</p>
|
|
)}
|
|
<div className="flex justify-end mt-1">
|
|
<Link
|
|
href="/forgot-password"
|
|
className="text-sm text-[rgb(20_113_76)] dark:text-emerald-400 hover:underline"
|
|
>
|
|
Forgot password?
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
|
|
<button type="submit" className="btn-primary w-full" disabled={loading}>
|
|
{loading ? 'Signing in...' : 'Sign in'}
|
|
</button>
|
|
</form>
|
|
|
|
<div className="mt-6 pt-4 border-t border-gray-200 dark:border-gray-700 space-y-3">
|
|
<div className="text-center">
|
|
<span className="text-sm text-gray-600 dark:text-gray-400">Don't have an account? </span>
|
|
<Link href="/register" className="text-sm font-medium text-[rgb(20_113_76)] dark:text-emerald-400 hover:underline">
|
|
Create account
|
|
</Link>
|
|
</div>
|
|
<Link
|
|
href="/fayda-setup"
|
|
className="flex items-center justify-center gap-2 text-sm text-gray-600 dark:text-gray-400 hover:text-[rgb(20_113_76)] dark:hover:text-emerald-400 transition-colors"
|
|
>
|
|
<ShieldCheck className="w-4 h-4" />
|
|
Already verified with Fayda? Set up your password
|
|
</Link>
|
|
</div>
|
|
|
|
<div className="mt-4 text-center">
|
|
<button
|
|
onClick={() => router.push('/booking/search')}
|
|
className="text-sm text-gray-600 dark:text-gray-400 hover:text-primary dark:hover:text-primary-400"
|
|
>
|
|
← Back to booking
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function LoginPage() {
|
|
return (
|
|
<Suspense fallback={
|
|
<div className="min-h-screen bg-gradient-to-br from-[rgb(20_113_76)] from-10% via-transparent to-[rgb(20_113_76)] to-90% dark:from-gray-900 dark:to-gray-800 flex items-center justify-center">
|
|
<div className="text-center">
|
|
<div className="w-12 h-12 bg-white rounded-lg flex items-center justify-center mx-auto mb-4">
|
|
<Train className="w-6 h-6 text-[rgb(20_113_76)]" />
|
|
</div>
|
|
<p className="text-gray-600 dark:text-gray-400">Loading...</p>
|
|
</div>
|
|
</div>
|
|
}>
|
|
<LoginContent />
|
|
</Suspense>
|
|
);
|
|
}
|