(Feat): Initial Commit
This commit is contained in:
352
src/pages/LoginPage.tsx
Normal file
352
src/pages/LoginPage.tsx
Normal file
@@ -0,0 +1,352 @@
|
||||
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">Work Allocation System</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer Info */}
|
||||
<div className="text-center">
|
||||
<p className="text-blue-200/40 text-xs">
|
||||
Secure login powered by JWT authentication
|
||||
</p>
|
||||
</div>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user