360 lines
14 KiB
TypeScript
360 lines
14 KiB
TypeScript
import { useState } from 'react';
|
|
import { Button } from '../ui/button';
|
|
import { Input } from '../ui/input';
|
|
import { Label } from '../ui/label';
|
|
import { Checkbox } from '../ui/checkbox';
|
|
import { AlertCircle, Copy, Check, Eye, EyeOff } from 'lucide-react';
|
|
import { mockUsers } from '../../lib/mock-data';
|
|
import { toast } from 'sonner';
|
|
|
|
interface LoginPageProps {
|
|
onLogin: (email: string, password: string) => void;
|
|
}
|
|
|
|
export function LoginPage({ onLogin }: LoginPageProps) {
|
|
const [email, setEmail] = useState('');
|
|
const [password, setPassword] = useState('');
|
|
const [rememberMe, setRememberMe] = useState(false);
|
|
const [error, setError] = useState('');
|
|
const [showForgotPassword, setShowForgotPassword] = useState(false);
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [showPassword, setShowPassword] = useState(false);
|
|
const [copiedIndex, setCopiedIndex] = useState<number | null>(null);
|
|
|
|
const copyToClipboard = async (text: string, index: number) => {
|
|
try {
|
|
// Try modern clipboard API first
|
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
await navigator.clipboard.writeText(text);
|
|
setCopiedIndex(index);
|
|
setTimeout(() => setCopiedIndex(null), 2000);
|
|
return;
|
|
}
|
|
} catch (err) {
|
|
// Clipboard API blocked, try fallback method
|
|
}
|
|
|
|
// Fallback method for older browsers or blocked clipboard
|
|
try {
|
|
const textArea = document.createElement('textarea');
|
|
textArea.value = text;
|
|
textArea.style.position = 'fixed';
|
|
textArea.style.left = '-999999px';
|
|
textArea.style.top = '-999999px';
|
|
document.body.appendChild(textArea);
|
|
textArea.focus();
|
|
textArea.select();
|
|
|
|
const successful = document.execCommand('copy');
|
|
document.body.removeChild(textArea);
|
|
|
|
if (successful) {
|
|
setCopiedIndex(index);
|
|
setTimeout(() => setCopiedIndex(null), 2000);
|
|
}
|
|
} catch (err) {
|
|
// Both methods failed - silently ignore
|
|
}
|
|
};
|
|
|
|
const quickLogin = async (userEmail: string, userPassword: string) => {
|
|
setEmail(userEmail);
|
|
setPassword(userPassword);
|
|
setError('');
|
|
setIsLoading(true);
|
|
try {
|
|
await onLogin(userEmail, userPassword);
|
|
} catch (err: any) {
|
|
const msg = err.response?.data?.message || err.message || 'Auto-login failed';
|
|
setError(msg);
|
|
toast.error(msg);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (isLoading) return;
|
|
|
|
setError('');
|
|
|
|
if (!email || !password) {
|
|
setError('Please enter both email and password');
|
|
return;
|
|
}
|
|
|
|
setIsLoading(true);
|
|
try {
|
|
await onLogin(email, password);
|
|
} catch (err: any) {
|
|
const msg = err.response?.data?.message || err.message || 'Login failed';
|
|
setError(msg);
|
|
toast.error(msg);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleForgotPassword = (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
// Mock password reset
|
|
alert('Password reset link sent to ' + email);
|
|
setShowForgotPassword(false);
|
|
};
|
|
|
|
return (
|
|
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 p-4 overflow-y-auto">
|
|
{/* Background decorative elements */}
|
|
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
|
<div className="absolute -top-40 -right-40 w-80 h-80 bg-amber-600/10 rounded-full blur-3xl"></div>
|
|
<div className="absolute -bottom-40 -left-40 w-80 h-80 bg-amber-600/10 rounded-full blur-3xl"></div>
|
|
</div>
|
|
|
|
<div className="relative w-full max-w-6xl grid md:grid-cols-2 gap-8 my-8">
|
|
{/* Left side - Login Form */}
|
|
<div className="flex flex-col">
|
|
{/* Logo and Header */}
|
|
<div className="text-center mb-8">
|
|
<div className="inline-flex items-center justify-center w-20 h-20 bg-amber-600 rounded-full mb-4">
|
|
<svg viewBox="0 0 24 24" className="w-12 h-12 text-white" fill="currentColor">
|
|
<path d="M12 2L4 6v6c0 5.5 3.8 10.7 8 12 4.2-1.3 8-6.5 8-12V6l-8-4zm0 2.2l6 3v4.8c0 4.5-3.1 8.7-6 10-2.9-1.3-6-5.5-6-10V7.2l6-3z" />
|
|
<circle cx="12" cy="12" r="3" />
|
|
</svg>
|
|
</div>
|
|
<h1 className="text-white mb-2">Royal Enfield</h1>
|
|
<p className="text-slate-400">Dealership Onboarding System</p>
|
|
</div>
|
|
|
|
{/* Login Form */}
|
|
<div className="bg-white rounded-lg shadow-2xl p-8">
|
|
{!showForgotPassword ? (
|
|
<form onSubmit={handleSubmit} className="space-y-6">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="email">Email Address</Label>
|
|
<Input
|
|
id="email"
|
|
type="email"
|
|
placeholder="you@royalenfield.com"
|
|
value={email}
|
|
onChange={(e) => setEmail(e.target.value)}
|
|
className="w-full"
|
|
disabled={isLoading}
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="password">Password</Label>
|
|
<div className="relative">
|
|
<Input
|
|
id="password"
|
|
type={showPassword ? "text" : "password"}
|
|
placeholder="Enter your password"
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
className="no-native-password-reveal w-full pr-10"
|
|
autoComplete="current-password"
|
|
disabled={isLoading}
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowPassword(!showPassword)}
|
|
className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600 focus:outline-none"
|
|
disabled={isLoading}
|
|
>
|
|
{showPassword ? (
|
|
<EyeOff className="w-5 h-5" />
|
|
) : (
|
|
<Eye className="w-5 h-5" />
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center space-x-2">
|
|
<Checkbox
|
|
id="remember"
|
|
checked={rememberMe}
|
|
onCheckedChange={(checked) => setRememberMe(checked as boolean)}
|
|
disabled={isLoading}
|
|
/>
|
|
<Label htmlFor="remember" className="cursor-pointer">
|
|
Remember Me
|
|
</Label>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowForgotPassword(true)}
|
|
className="text-amber-600 hover:text-amber-700 disabled:opacity-50"
|
|
disabled={isLoading}
|
|
>
|
|
Forgot Password?
|
|
</button>
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="flex items-center gap-2 p-3 bg-red-50 border border-red-200 rounded-md">
|
|
<AlertCircle className="w-4 h-4 text-red-600" />
|
|
<span className="text-red-600 font-medium text-sm">{error}</span>
|
|
</div>
|
|
)}
|
|
|
|
<Button
|
|
type="submit"
|
|
className="w-full bg-amber-600 hover:bg-amber-700 h-11"
|
|
disabled={isLoading}
|
|
>
|
|
{isLoading ? (
|
|
<div className="flex items-center justify-center gap-2">
|
|
<div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
|
<span>Logging in...</span>
|
|
</div>
|
|
) : (
|
|
'Login'
|
|
)}
|
|
</Button>
|
|
|
|
<div className="text-center">
|
|
<div className="relative my-4">
|
|
<div className="absolute inset-0 flex items-center">
|
|
<span className="w-full border-t border-slate-200" />
|
|
</div>
|
|
<div className="relative flex justify-center text-xs uppercase">
|
|
<span className="bg-white px-2 text-slate-500">Or</span>
|
|
</div>
|
|
</div>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
className="w-full border-amber-600 text-amber-600 hover:bg-amber-50 h-11"
|
|
onClick={() => window.location.href = '/prospective-login'}
|
|
>
|
|
Prospective User Login
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
) : (
|
|
<form onSubmit={handleForgotPassword} className="space-y-6">
|
|
<div>
|
|
<h2 className="mb-2">Reset Password</h2>
|
|
<p className="text-slate-600">Enter your email to receive a password reset link</p>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="reset-email">Email Address</Label>
|
|
<Input
|
|
id="reset-email"
|
|
type="email"
|
|
placeholder="you@royalenfield.com"
|
|
value={email}
|
|
onChange={(e) => setEmail(e.target.value)}
|
|
required
|
|
className="w-full"
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex gap-3">
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
onClick={() => setShowForgotPassword(false)}
|
|
className="flex-1"
|
|
>
|
|
Back to Login
|
|
</Button>
|
|
<Button type="submit" className="flex-1 bg-amber-600 hover:bg-amber-700">
|
|
Send Reset Link
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
)}
|
|
</div>
|
|
|
|
{/* Footer */}
|
|
<div className="text-center mt-6 text-slate-400">
|
|
<p>© 2025 Royal Enfield. All rights reserved.</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Right side - Test Credentials */}
|
|
<div className="bg-white rounded-lg shadow-2xl p-8 overflow-y-auto max-h-[800px]">
|
|
<div className="mb-6">
|
|
<h2 className="mb-2">Test User Credentials</h2>
|
|
<p className="text-slate-600">Click on any user to auto-login</p>
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
{mockUsers.map((user, index) => (
|
|
<div
|
|
key={user.id}
|
|
className="border border-slate-200 rounded-lg p-4 hover:border-amber-600 hover:bg-amber-50 transition-all cursor-pointer"
|
|
onClick={() => quickLogin(user.email, user.password)}
|
|
>
|
|
<div className="flex items-start justify-between mb-2">
|
|
<div className="flex-1">
|
|
<div className="flex items-center gap-2 mb-1">
|
|
<span className="px-2 py-1 bg-amber-100 text-amber-800 rounded text-xs">
|
|
{user.role}
|
|
</span>
|
|
</div>
|
|
<p className="text-slate-900">{user.name}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<div className="flex items-center justify-between gap-2">
|
|
<div className="flex-1">
|
|
<p className="text-slate-500">Email:</p>
|
|
<p className="text-slate-900 font-mono break-all">{user.email}</p>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
copyToClipboard(user.email, index * 2);
|
|
}}
|
|
className="p-2 hover:bg-slate-100 rounded"
|
|
>
|
|
{copiedIndex === index * 2 ? (
|
|
<Check className="w-4 h-4 text-green-600" />
|
|
) : (
|
|
<Copy className="w-4 h-4 text-slate-400" />
|
|
)}
|
|
</button>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between gap-2">
|
|
<div className="flex-1">
|
|
<p className="text-slate-500">Password:</p>
|
|
<p className="text-slate-900 font-mono">{user.password}</p>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
copyToClipboard(user.password, index * 2 + 1);
|
|
}}
|
|
className="p-2 hover:bg-slate-100 rounded"
|
|
>
|
|
{copiedIndex === index * 2 + 1 ? (
|
|
<Check className="w-4 h-4 text-green-600" />
|
|
) : (
|
|
<Copy className="w-4 h-4 text-slate-400" />
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-3 pt-3 border-t border-slate-200">
|
|
<p className="text-amber-600 text-center">Click to login as {user.role}</p>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|