348 lines
15 KiB
TypeScript
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>
|
|
);
|
|
};
|