224 lines
11 KiB
JavaScript
224 lines
11 KiB
JavaScript
import React, { useState } from 'react';
|
|
import { useLocation, useNavigate } from 'react-router-dom';
|
|
import { AlertCircle, ArrowRight, Loader2, Eye, EyeOff, Sparkles, CheckCircle2 } from 'lucide-react';
|
|
import { cn } from '@/lib/utils';
|
|
import { KROW_LOGO_URL } from '@/assets/brand';
|
|
import { Checkbox } from '@/components/ds';
|
|
import { base44 } from '@/api/base44Client';
|
|
import { startAdminSession } from '@/pages/admin/session';
|
|
|
|
const DEMO_EMAIL = 'demo@krow.app';
|
|
|
|
export default function AdminLogin() {
|
|
const navigate = useNavigate();
|
|
const location = useLocation();
|
|
|
|
const from = location.state?.from;
|
|
const returnTo = typeof from === 'string' && from.startsWith('/admin') && from !== '/admin/login'
|
|
? from
|
|
: '/admin';
|
|
|
|
const [form, setForm] = useState({ email: DEMO_EMAIL, password: '', remember: true });
|
|
const [showPassword, setShowPassword] = useState(false);
|
|
const [errors, setErrors] = useState({});
|
|
const [authError, setAuthError] = useState('');
|
|
const [status, setStatus] = useState('idle');
|
|
|
|
const validate = () => {
|
|
const next = {};
|
|
if (!form.email.trim()) next.email = 'Enter your work email';
|
|
else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email.trim())) next.email = 'Enter a valid email address';
|
|
if (!form.password) next.password = 'Enter your password';
|
|
else if (form.password.length < 8) next.password = 'Passwords are at least 8 characters';
|
|
setErrors(next);
|
|
return Object.keys(next).length === 0;
|
|
};
|
|
|
|
const submit = async (event) => {
|
|
event.preventDefault();
|
|
setAuthError('');
|
|
if (!validate()) return;
|
|
|
|
setStatus('loading');
|
|
await new Promise((r) => setTimeout(r, 900));
|
|
|
|
if (form.password.toLowerCase() === 'wrong') {
|
|
setStatus('idle');
|
|
setAuthError('That email and password do not match an administrator account.');
|
|
return;
|
|
}
|
|
|
|
await base44.auth.updateMe({ account_type: 'admin' });
|
|
localStorage.setItem('activeRole', 'admin');
|
|
startAdminSession();
|
|
setStatus('success');
|
|
setTimeout(() => navigate(returnTo, { replace: true }), 320);
|
|
};
|
|
|
|
const busy = status !== 'idle';
|
|
|
|
return (
|
|
<main className="min-h-screen w-full bg-[#FAF9F5] flex items-center justify-center p-4 sm:p-6 lg:p-8 font-sans selection:bg-[#F9E547] selection:text-[#0F172A]">
|
|
<div className="w-full max-w-5xl bg-white border border-[#F3E8C8] rounded-3xl shadow-xl shadow-amber-900/5 overflow-hidden grid grid-cols-1 md:grid-cols-2 min-h-[580px]">
|
|
{/* Left Visual Section — White & Yellow Theme */}
|
|
<div className="relative hidden md:flex flex-col justify-between p-8 sm:p-10 bg-gradient-to-br from-[#FFFDF7] via-[#FFFBEB] to-[#FEF3C7] overflow-hidden border-r border-[#F3E8C8]">
|
|
{/* Subtle Decorative Yellow Orb / Accents */}
|
|
<div className="absolute -top-20 -left-20 w-64 h-64 rounded-full bg-[#F9E547]/30 blur-3xl pointer-events-none" />
|
|
<div className="absolute -bottom-20 -right-20 w-80 h-80 rounded-full bg-[#F59E0B]/20 blur-3xl pointer-events-none" />
|
|
|
|
{/* Top Brand & Badge */}
|
|
<div className="relative z-10 flex items-center justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<img src={KROW_LOGO_URL} alt="KROW" className="h-8 w-auto object-contain" />
|
|
<span className="text-[11px] font-extrabold uppercase tracking-widest px-3 py-1 rounded-full bg-[#F9E547] text-[#0F172A] border border-[#FACC15] shadow-sm">
|
|
Enterprise
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Central KROW Visual Image */}
|
|
<div className="relative z-10 my-auto py-6 flex flex-col items-center">
|
|
<div className="relative rounded-2xl overflow-hidden shadow-lg border-2 border-white bg-white p-2 max-w-xs">
|
|
<img
|
|
src="/krow%20logo.jpeg"
|
|
alt="KROW Logo Visual"
|
|
className="w-full h-auto object-cover rounded-xl transition-transform duration-500 hover:scale-105"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Bottom Hero Tagline */}
|
|
<div className="relative z-10 space-y-2">
|
|
<div className="flex items-center gap-1.5 text-xs font-bold text-[#D97706] uppercase tracking-wider">
|
|
<Sparkles className="w-3.5 h-3.5" />
|
|
Recruiting Intelligence Platform
|
|
</div>
|
|
<h2 className="text-xl font-bold text-[#1E293B] tracking-tight leading-snug">
|
|
Workforce decisions powered by intelligence
|
|
</h2>
|
|
<p className="text-xs text-[#64748B] leading-relaxed">
|
|
AI-driven candidate screening, verified score dimensions, and talent matching.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Right Login Form Section */}
|
|
<div className="flex flex-col justify-center p-6 sm:p-10 lg:p-12 bg-white">
|
|
<div className="w-full max-w-md mx-auto space-y-6">
|
|
{/* Header */}
|
|
<div>
|
|
<div className="flex items-center gap-2 mb-3 md:hidden">
|
|
<img src={KROW_LOGO_URL} alt="KROW" className="h-7 w-auto object-contain" />
|
|
<span className="text-[10px] font-bold uppercase tracking-widest px-2 py-0.5 rounded bg-[#F9E547] text-[#0F172A]">
|
|
Admin
|
|
</span>
|
|
</div>
|
|
<h1 className="text-2xl font-extrabold text-[#0F172A] tracking-tight">Welcome back</h1>
|
|
<p className="mt-1.5 text-xs text-[#64748B] leading-relaxed">
|
|
Sign in to manage hiring, candidates, talent intelligence, and workforce decisions.
|
|
</p>
|
|
</div>
|
|
|
|
{/* Error Alert */}
|
|
{authError && (
|
|
<div role="alert" className="flex items-start gap-2.5 rounded-xl border border-red-200 bg-red-50 p-3.5 text-red-800">
|
|
<AlertCircle className="mt-0.5 h-4 w-4 shrink-0 text-red-600" aria-hidden="true" />
|
|
<p className="text-xs leading-relaxed">{authError}</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Form */}
|
|
<form onSubmit={submit} noValidate className="space-y-4">
|
|
{/* Email */}
|
|
<div className="space-y-1.5">
|
|
<label className="block text-xs font-bold text-[#334155] uppercase tracking-wider">
|
|
Work email
|
|
</label>
|
|
<input
|
|
type="email"
|
|
autoComplete="username"
|
|
autoFocus
|
|
disabled={busy}
|
|
value={form.email}
|
|
onChange={(e) => setForm({ ...form, email: e.target.value })}
|
|
className={cn(
|
|
"w-full h-11 px-3.5 rounded-xl bg-[#FAF9F5] border text-sm text-[#0F172A] placeholder-slate-400 focus:outline-none transition-all",
|
|
errors.email ? "border-red-500 focus:border-red-500" : "border-[#E2E8F0] focus:border-[#F59E0B] focus:ring-2 focus:ring-[#FEF3C7]"
|
|
)}
|
|
placeholder="name@company.com"
|
|
/>
|
|
{errors.email && <p className="text-xs text-red-600 font-medium">{errors.email}</p>}
|
|
</div>
|
|
|
|
{/* Password */}
|
|
<div className="space-y-1.5">
|
|
<label className="block text-xs font-bold text-[#334155] uppercase tracking-wider">
|
|
Password
|
|
</label>
|
|
<div className="relative">
|
|
<input
|
|
type={showPassword ? "text" : "password"}
|
|
autoComplete="current-password"
|
|
disabled={busy}
|
|
value={form.password}
|
|
onChange={(e) => setForm({ ...form, password: e.target.value })}
|
|
className={cn(
|
|
"w-full h-11 pl-3.5 pr-10 rounded-xl bg-[#FAF9F5] border text-sm text-[#0F172A] placeholder-slate-400 focus:outline-none transition-all",
|
|
errors.password ? "border-red-500 focus:border-red-500" : "border-[#E2E8F0] focus:border-[#F59E0B] focus:ring-2 focus:ring-[#FEF3C7]"
|
|
)}
|
|
placeholder="••••••••"
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowPassword(!showPassword)}
|
|
className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 hover:text-[#0F172A] transition-colors"
|
|
>
|
|
{showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
|
</button>
|
|
</div>
|
|
{errors.password && <p className="text-xs text-red-600 font-medium">{errors.password}</p>}
|
|
</div>
|
|
|
|
{/* Remember & Forgot */}
|
|
<div className="flex items-center justify-between text-xs pt-1">
|
|
<label className="flex cursor-pointer items-center gap-2 text-[#475569] font-medium">
|
|
<Checkbox
|
|
checked={form.remember}
|
|
onCheckedChange={(v) => setForm({ ...form, remember: Boolean(v) })}
|
|
disabled={busy}
|
|
/>
|
|
Remember me
|
|
</label>
|
|
<button
|
|
type="button"
|
|
disabled={busy}
|
|
onClick={() => setAuthError('Password resets are disabled in the demo.')}
|
|
className="text-[#D97706] hover:text-[#B45309] font-semibold transition-colors"
|
|
>
|
|
Forgot password?
|
|
</button>
|
|
</div>
|
|
|
|
{/* Primary CTA Button — KROW Yellow Theme */}
|
|
<button
|
|
type="submit"
|
|
disabled={busy}
|
|
className="w-full h-11 mt-3 rounded-xl bg-[#F9E547] hover:bg-[#FACC15] active:bg-[#EAB308] disabled:opacity-50 text-[#0F172A] text-sm font-bold flex items-center justify-center gap-2 border border-[#EAB308]/40 shadow-md shadow-amber-300/40 transition-all cursor-pointer"
|
|
>
|
|
{status === 'loading' && <Loader2 className="w-4 h-4 animate-spin text-[#0F172A]" />}
|
|
{status === 'success' && <CheckCircle2 className="w-4 h-4 text-[#0F172A]" />}
|
|
<span>{status === 'loading' ? 'Signing in...' : status === 'success' ? 'Signed in' : 'Continue'}</span>
|
|
{status === 'idle' && <ArrowRight className="w-4 h-4" />}
|
|
</button>
|
|
</form>
|
|
|
|
<p className="text-center text-xs text-[#94A3B8] leading-relaxed pt-2">
|
|
Demo environment — enter any 8+ character password to sign in. Use <span className="text-[#64748B] font-semibold">wrong</span> to test error validation.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
);
|
|
}
|