148 lines
6.5 KiB
JavaScript
148 lines
6.5 KiB
JavaScript
import React, { useState } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { useAuth } from '../context/useAuth';
|
|
import { ShieldCheck, UserCheck, Lock, User, ArrowRight, Sparkles } from 'lucide-react';
|
|
|
|
export function LoginPage() {
|
|
const navigate = useNavigate();
|
|
const { login } = useAuth();
|
|
|
|
const [selectedRole, setSelectedRole] = useState('admin');
|
|
const [username, setUsername] = useState('');
|
|
const [password, setPassword] = useState('');
|
|
const [error, setError] = useState('');
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
const handleRoleSelect = (role) => {
|
|
setSelectedRole(role);
|
|
setError('');
|
|
};
|
|
|
|
const handleFormSubmit = async (e) => {
|
|
e.preventDefault();
|
|
setError('');
|
|
if (!username.trim() || !password.trim()) {
|
|
setError('Please enter both username and password.');
|
|
return;
|
|
}
|
|
|
|
setLoading(true);
|
|
|
|
try {
|
|
const user = await login(username, password);
|
|
setLoading(false);
|
|
// Route by the role the SERVER assigned, not by the tab that was
|
|
// selected. The tab only labels which account you are signing in to.
|
|
if (user.role === 'admin') navigate('/admin');
|
|
else navigate('/user');
|
|
} catch (err) {
|
|
setLoading(false);
|
|
// Surface the server's own message: it distinguishes bad credentials
|
|
// from the lockout after repeated failures, which a generic string hides.
|
|
setError(err?.message || 'Login failed. Check your username and password.');
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="min-h-screen w-full bg-slate-950 text-slate-100 flex items-center justify-center p-4 relative overflow-hidden font-sans">
|
|
{/* Background Glow Accents */}
|
|
<div className="absolute -top-40 -left-40 h-96 w-96 rounded-full bg-amber-500/10 blur-3xl pointer-events-none" />
|
|
<div className="absolute -bottom-40 -right-40 h-96 w-96 rounded-full bg-emerald-500/10 blur-3xl pointer-events-none" />
|
|
|
|
<div className="w-full max-w-md relative z-10">
|
|
{/* Header Branding */}
|
|
<div className="text-center mb-8">
|
|
<div className="inline-flex items-center gap-2 px-3.5 py-1 rounded-full bg-amber-500/10 border border-amber-500/30 text-amber-400 text-xs font-semibold mb-3">
|
|
<Sparkles className="h-3.5 w-3.5" /> Brand Catalog & Store Intelligence
|
|
</div>
|
|
<h1 className="text-3xl font-bold tracking-tight text-white font-display">System Authentication</h1>
|
|
<p className="text-sm text-slate-400 mt-1">Please enter your username and password to sign in</p>
|
|
</div>
|
|
|
|
{/* Main Card */}
|
|
<div className="rounded-2xl border border-slate-800 bg-slate-900/80 p-6 backdrop-blur-xl shadow-2xl">
|
|
{/* Role Selection Tabs: Admin and User */}
|
|
<div className="grid grid-cols-2 gap-2 p-1.5 rounded-xl bg-slate-950/80 border border-slate-800/80 mb-6">
|
|
<button
|
|
type="button"
|
|
onClick={() => handleRoleSelect('admin')}
|
|
className={`flex items-center justify-center gap-2 py-3 px-4 rounded-lg text-xs font-bold transition-all ${
|
|
selectedRole === 'admin'
|
|
? 'bg-amber-500 text-slate-950 shadow-md scale-[1.02]'
|
|
: 'text-slate-400 hover:text-white hover:bg-slate-800/50'
|
|
}`}
|
|
>
|
|
<ShieldCheck className="h-4 w-4" /> Admin Account
|
|
</button>
|
|
|
|
<button
|
|
type="button"
|
|
onClick={() => handleRoleSelect('user')}
|
|
className={`flex items-center justify-center gap-2 py-3 px-4 rounded-lg text-xs font-bold transition-all ${
|
|
selectedRole === 'user'
|
|
? 'bg-blue-500 text-white shadow-md scale-[1.02]'
|
|
: 'text-slate-400 hover:text-white hover:bg-slate-800/50'
|
|
}`}
|
|
>
|
|
<UserCheck className="h-4 w-4" /> User Account
|
|
</button>
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="mb-4 rounded-lg bg-rose-500/10 border border-rose-500/30 p-3 text-xs text-rose-400">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
{/* Login Form */}
|
|
<form onSubmit={handleFormSubmit} className="space-y-4">
|
|
<div>
|
|
<label className="block text-xs font-medium text-slate-300 mb-1.5">Username</label>
|
|
<div className="relative">
|
|
<User className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-slate-500" />
|
|
<input
|
|
type="text"
|
|
required
|
|
value={username}
|
|
onChange={(e) => setUsername(e.target.value)}
|
|
placeholder={`Enter ${selectedRole} username`}
|
|
className="w-full rounded-xl border border-slate-700 bg-slate-950/80 py-2.5 pl-9 pr-4 text-sm text-white placeholder-slate-500 focus:border-amber-500 focus:outline-none focus:ring-1 focus:ring-amber-500"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-xs font-medium text-slate-300 mb-1.5">Password</label>
|
|
<div className="relative">
|
|
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-slate-500" />
|
|
<input
|
|
type="password"
|
|
required
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
placeholder="Enter password"
|
|
className="w-full rounded-xl border border-slate-700 bg-slate-950/80 py-2.5 pl-9 pr-4 text-sm text-white placeholder-slate-500 focus:border-amber-500 focus:outline-none focus:ring-1 focus:ring-amber-500"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<button
|
|
type="submit"
|
|
disabled={loading}
|
|
className="w-full flex items-center justify-center gap-2 rounded-xl bg-gradient-to-r from-amber-500 to-amber-600 hover:from-amber-600 hover:to-amber-700 text-slate-950 py-3 text-sm font-bold shadow-lg shadow-amber-500/20 transition-all duration-200 active:scale-98 disabled:opacity-50"
|
|
>
|
|
{loading ? 'Authenticating...' : `Sign In as ${selectedRole.toUpperCase()}`}
|
|
<ArrowRight className="h-4 w-4" />
|
|
</button>
|
|
</form>
|
|
</div>
|
|
|
|
{/* Footer info */}
|
|
<p className="text-center text-xs text-slate-500 mt-6">
|
|
Powered by pgvector RAG Model & Multi-Store Nutrition Intelligence Engine
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|