Files
EmployeeManagementSystem/src/pages/LoginPage.tsx

348 lines
15 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import { useAuth } from '../contexts/AuthContext';
import {
Users, Lock, Eye, EyeOff, XCircle, Mail, ArrowRight,
CheckCircle, X, Sparkles, Shield, KeyRound
} from 'lucide-react';
export const LoginPage: React.FC = () => {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [showPassword, setShowPassword] = useState(false);
const [rememberMe, setRememberMe] = useState(false);
const [error, setError] = useState('');
const [showError, setShowError] = useState(false);
const [loading, setLoading] = useState(false);
const { login } = useAuth();
// Forgot password modal state
const [showForgotModal, setShowForgotModal] = useState(false);
const [forgotEmail, setForgotEmail] = useState('');
const [forgotLoading, setForgotLoading] = useState(false);
const [forgotSuccess, setForgotSuccess] = useState(false);
const [forgotError, setForgotError] = useState('');
// Auto-hide error after 5 seconds
useEffect(() => {
if (error) {
setShowError(true);
const timer = setTimeout(() => {
setShowError(false);
setTimeout(() => setError(''), 300);
}, 5000);
return () => clearTimeout(timer);
}
}, [error]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
setLoading(true);
try {
await login(username, password);
} catch (err: unknown) {
const error = err as Error;
const errorMessage = error.message?.includes('401') || error.message?.includes('Unauthorized') || error.message?.includes('Invalid')
? 'Invalid username or password'
: error.message || 'Login failed. Please check your credentials.';
setError(errorMessage);
console.error('Login error:', err);
} finally {
setLoading(false);
}
};
const handleForgotPassword = async (e: React.FormEvent) => {
e.preventDefault();
setForgotLoading(true);
setForgotError('');
// Simulate API call (replace with actual API call)
try {
await new Promise(resolve => setTimeout(resolve, 1500));
// In a real app, you'd call: await api.requestPasswordReset(forgotEmail);
setForgotSuccess(true);
} catch {
setForgotError('Failed to send reset email. Please try again.');
} finally {
setForgotLoading(false);
}
};
const closeForgotModal = () => {
setShowForgotModal(false);
setForgotEmail('');
setForgotSuccess(false);
setForgotError('');
};
return (
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-blue-900 to-slate-900 flex items-center justify-center p-4 relative overflow-hidden">
{/* Animated background elements */}
<div className="absolute inset-0 overflow-hidden">
<div className="absolute -top-40 -right-40 w-80 h-80 bg-blue-500/20 rounded-full blur-3xl animate-pulse" />
<div className="absolute -bottom-40 -left-40 w-80 h-80 bg-purple-500/20 rounded-full blur-3xl animate-pulse delay-1000" />
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-96 h-96 bg-cyan-500/10 rounded-full blur-3xl animate-pulse delay-500" />
</div>
{/* Floating particles */}
<div className="absolute inset-0 overflow-hidden pointer-events-none">
{[...Array(20)].map((_, i) => (
<div
key={i}
className="absolute w-1 h-1 bg-white/20 rounded-full animate-float"
style={{
left: `${Math.random() * 100}%`,
top: `${Math.random() * 100}%`,
animationDelay: `${Math.random() * 5}s`,
animationDuration: `${5 + Math.random() * 10}s`,
}}
/>
))}
</div>
<div className="w-full max-w-md relative z-10">
{/* Login Card */}
<div className="bg-white/10 backdrop-blur-xl rounded-3xl shadow-2xl p-10 border border-white/20">
{/* Logo & Title */}
<div className="text-center mb-8">
<div className="inline-flex items-center justify-center w-20 h-20 bg-gradient-to-br from-blue-500 to-purple-600 rounded-2xl shadow-lg mb-4 relative">
<Shield size={40} className="text-white" strokeWidth={1.5} />
<Sparkles size={16} className="text-yellow-300 absolute -top-1 -right-1 animate-pulse" />
</div>
<h1 className="text-2xl font-bold text-white mb-1">Welcome Back</h1>
<p className="text-blue-200/70 text-sm">Sign in to your account to continue</p>
</div>
{/* Login Form */}
<form onSubmit={handleSubmit} className="space-y-5">
{/* Username Input */}
<div className="relative group">
<div className="absolute left-4 top-1/2 -translate-y-1/2 text-blue-300/70 group-focus-within:text-blue-400 transition-colors">
<Users size={20} />
</div>
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="Username"
required
autoComplete="username"
className="w-full pl-12 pr-4 py-4 bg-white/10 border border-white/20 rounded-xl text-white placeholder-blue-200/50 focus:outline-none focus:ring-2 focus:ring-blue-400/50 focus:border-blue-400/50 focus:bg-white/15 transition-all"
/>
</div>
{/* Password Input */}
<div className="relative group">
<div className="absolute left-4 top-1/2 -translate-y-1/2 text-blue-300/70 group-focus-within:text-blue-400 transition-colors">
<Lock size={20} />
</div>
<input
type={showPassword ? 'text' : 'password'}
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
required
autoComplete="current-password"
className="w-full pl-12 pr-12 py-4 bg-white/10 border border-white/20 rounded-xl text-white placeholder-blue-200/50 focus:outline-none focus:ring-2 focus:ring-blue-400/50 focus:border-blue-400/50 focus:bg-white/15 transition-all"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-4 top-1/2 -translate-y-1/2 text-blue-300/70 hover:text-blue-300 transition-colors"
>
{showPassword ? <EyeOff size={20} /> : <Eye size={20} />}
</button>
</div>
{/* Remember Me & Forgot Password */}
<div className="flex items-center justify-between text-sm">
<label className="flex items-center cursor-pointer group">
<input
type="checkbox"
checked={rememberMe}
onChange={(e) => setRememberMe(e.target.checked)}
className="w-4 h-4 bg-white/10 border-white/30 rounded text-blue-500 focus:ring-blue-400/50 focus:ring-offset-0"
/>
<span className="ml-2 text-blue-200/70 group-hover:text-blue-200 transition-colors">Remember me</span>
</label>
<button
type="button"
onClick={() => setShowForgotModal(true)}
className="text-blue-300 hover:text-blue-200 transition-colors hover:underline"
>
Forgot password?
</button>
</div>
{/* Login Button */}
<button
type="submit"
disabled={loading || !username || !password}
className="w-full bg-gradient-to-r from-blue-500 via-blue-600 to-purple-600 hover:from-blue-600 hover:via-blue-700 hover:to-purple-700 text-white font-semibold py-4 rounded-xl shadow-lg hover:shadow-blue-500/25 transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2 group"
>
{loading ? (
<>
<div className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
Signing in...
</>
) : (
<>
Sign In
<ArrowRight size={18} className="group-hover:translate-x-1 transition-transform" />
</>
)}
</button>
</form>
{/* Divider */}
<div className="relative my-8">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-white/10" />
</div>
<div className="relative flex justify-center text-xs">
<span className="px-4 bg-transparent text-blue-200/50"></span>
</div>
</div>
{/* Footer Info */}
</div>
{/* Version badge */}
<div className="text-center mt-6">
<span className="text-blue-300/30 text-xs">v2.0.0</span>
</div>
</div>
{/* Error Toast */}
{error && (
<div className={`fixed top-6 left-1/2 transform -translate-x-1/2 z-50 transition-all duration-300 ${showError ? 'opacity-100 translate-y-0' : 'opacity-0 -translate-y-4'}`}>
<div className="bg-gradient-to-r from-red-500 to-red-600 text-white px-6 py-4 rounded-2xl shadow-2xl flex items-center gap-3 min-w-[320px] border border-red-400/30">
<div className="w-10 h-10 bg-white/20 rounded-full flex items-center justify-center flex-shrink-0">
<XCircle size={24} />
</div>
<div className="flex-1">
<p className="font-semibold">Login Failed</p>
<p className="text-sm text-red-100">{error}</p>
</div>
</div>
</div>
)}
{/* Forgot Password Modal */}
{showForgotModal && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
{/* Backdrop */}
<div
className="absolute inset-0 bg-black/60 backdrop-blur-sm"
onClick={closeForgotModal}
/>
{/* Modal */}
<div className="relative bg-slate-800/90 backdrop-blur-xl rounded-2xl shadow-2xl p-8 w-full max-w-md border border-white/10 animate-in fade-in zoom-in duration-200">
{/* Close button */}
<button
onClick={closeForgotModal}
className="absolute top-4 right-4 text-gray-400 hover:text-white transition-colors"
>
<X size={24} />
</button>
{!forgotSuccess ? (
<>
{/* Header */}
<div className="text-center mb-6">
<div className="inline-flex items-center justify-center w-16 h-16 bg-gradient-to-br from-amber-500 to-orange-600 rounded-2xl shadow-lg mb-4">
<KeyRound size={32} className="text-white" />
</div>
<h2 className="text-xl font-bold text-white mb-2">Forgot Password?</h2>
<p className="text-gray-400 text-sm">
Enter your email address and we'll send you instructions to reset your password.
</p>
</div>
{/* Form */}
<form onSubmit={handleForgotPassword} className="space-y-4">
<div className="relative">
<div className="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400">
<Mail size={20} />
</div>
<input
type="email"
value={forgotEmail}
onChange={(e) => setForgotEmail(e.target.value)}
placeholder="Enter your email"
required
className="w-full pl-12 pr-4 py-4 bg-white/10 border border-white/20 rounded-xl text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-amber-400/50 focus:border-amber-400/50 transition-all"
/>
</div>
{forgotError && (
<p className="text-red-400 text-sm text-center">{forgotError}</p>
)}
<button
type="submit"
disabled={forgotLoading || !forgotEmail}
className="w-full bg-gradient-to-r from-amber-500 to-orange-600 hover:from-amber-600 hover:to-orange-700 text-white font-semibold py-4 rounded-xl shadow-lg transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
>
{forgotLoading ? (
<>
<div className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
Sending...
</>
) : (
<>
<Mail size={18} />
Send Reset Link
</>
)}
</button>
</form>
{/* Back to login */}
<button
onClick={closeForgotModal}
className="w-full mt-4 text-gray-400 hover:text-white text-sm transition-colors"
>
← Back to login
</button>
</>
) : (
/* Success State */
<div className="text-center py-4">
<div className="inline-flex items-center justify-center w-16 h-16 bg-gradient-to-br from-green-500 to-emerald-600 rounded-full shadow-lg mb-4">
<CheckCircle size={32} className="text-white" />
</div>
<h2 className="text-xl font-bold text-white mb-2">Check Your Email</h2>
<p className="text-gray-400 text-sm mb-6">
We've sent password reset instructions to<br />
<span className="text-white font-medium">{forgotEmail}</span>
</p>
<button
onClick={closeForgotModal}
className="w-full bg-gradient-to-r from-green-500 to-emerald-600 hover:from-green-600 hover:to-emerald-700 text-white font-semibold py-4 rounded-xl shadow-lg transition-all duration-300"
>
Back to Login
</button>
</div>
)}
</div>
</div>
)}
{/* CSS for floating animation */}
<style>{`
@keyframes float {
0%, 100% { transform: translateY(0px) rotate(0deg); opacity: 0.2; }
50% { transform: translateY(-20px) rotate(180deg); opacity: 0.5; }
}
.animate-float {
animation: float linear infinite;
}
`}</style>
</div>
);
};