- inline ZinoLogo SVG (from sm2 renderer asset); replace aria badge in sidebar (centered) and login brand block - remove search + notification buttons from topbar - carry pending field prefill (field_defaults.value) + qualify RBAC trim Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
167 lines
6.9 KiB
TypeScript
167 lines
6.9 KiB
TypeScript
import { useState } from 'react';
|
|
import type { FormEvent } from 'react';
|
|
import { useLocation, useNavigate } from 'react-router-dom';
|
|
import { ArrowRight, CalendarClock, Eye, EyeOff, Lock, Mail, ShieldCheck, Sparkles } from 'lucide-react';
|
|
import { Button } from '../components';
|
|
import { ZinoLogo } from '../components/ZinoLogo';
|
|
import { useZino } from '../api/provider';
|
|
|
|
const HIGHLIGHTS = [
|
|
{ Icon: Sparkles, title: 'AI employees do the legwork', body: 'Aria qualifies, engages and schedules leads autonomously.' },
|
|
{ Icon: ShieldCheck, title: 'Capture → underwrite → issue', body: 'The whole policy lifecycle in one guided console.' },
|
|
{ Icon: CalendarClock, title: 'Live pipeline & agent scheduling', body: 'Every lead, meeting and decision as it happens.' },
|
|
];
|
|
|
|
function Field({
|
|
label,
|
|
icon,
|
|
trailing,
|
|
...rest
|
|
}: {
|
|
label: string;
|
|
icon: React.ReactNode;
|
|
trailing?: React.ReactNode;
|
|
} & React.InputHTMLAttributes<HTMLInputElement>) {
|
|
return (
|
|
<label className="flex flex-col gap-1.5">
|
|
<span className="text-xs font-medium text-on-navy-muted">{label}</span>
|
|
<div className="flex items-center gap-2.5 h-[48px] px-3.5 rounded-xl bg-white/[0.07] border border-white/15 transition-colors duration-150 focus-within:border-sunrise-400/70 focus-within:bg-white/[0.12]">
|
|
<span className="text-on-navy-muted shrink-0">{icon}</span>
|
|
<input
|
|
className="flex-1 w-full bg-transparent border-none outline-none text-base text-on-navy placeholder:text-white/35"
|
|
{...rest}
|
|
/>
|
|
{trailing}
|
|
</div>
|
|
</label>
|
|
);
|
|
}
|
|
|
|
export function Login() {
|
|
const { login } = useZino();
|
|
const navigate = useNavigate();
|
|
const location = useLocation();
|
|
const from = (location.state as { from?: string })?.from ?? '/pipeline';
|
|
|
|
const [email, setEmail] = useState('admin@getzino.com');
|
|
const [password, setPassword] = useState('Zino');
|
|
const [show, setShow] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [busy, setBusy] = useState(false);
|
|
|
|
async function onSubmit(e: FormEvent) {
|
|
e.preventDefault();
|
|
setBusy(true);
|
|
setError(null);
|
|
try {
|
|
await login(email, password);
|
|
navigate(from, { replace: true });
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : ((err as { message?: string })?.message ?? 'Login failed'));
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="relative min-h-screen overflow-hidden bg-navy-grad font-sans flex items-center justify-center p-6">
|
|
{/* subtle grid, faded toward the edges */}
|
|
<div
|
|
className="absolute inset-0 pointer-events-none"
|
|
style={{
|
|
backgroundImage:
|
|
'linear-gradient(to right, rgba(255,255,255,0.04) 1px, transparent 1px), linear-gradient(to bottom, rgba(255,255,255,0.04) 1px, transparent 1px)',
|
|
backgroundSize: '52px 52px',
|
|
maskImage: 'radial-gradient(ellipse 70% 60% at 50% 45%, #000 40%, transparent 100%)',
|
|
WebkitMaskImage: 'radial-gradient(ellipse 70% 60% at 50% 45%, #000 40%, transparent 100%)',
|
|
}}
|
|
/>
|
|
{/* one soft static glow for warmth */}
|
|
<div className="absolute top-[-15%] left-1/2 -translate-x-1/2 w-[640px] h-[440px] rounded-full bg-sunrise-500/12 blur-[140px] pointer-events-none" />
|
|
|
|
{/* one glass container holding the brand story (left) + the form (right) */}
|
|
<div className="relative w-full max-w-[920px] rounded-2xl bg-white/[0.07] backdrop-blur-2xl border border-white/15 shadow-2xl overflow-hidden grid lg:grid-cols-2">
|
|
{/* brand story */}
|
|
<div className="hidden lg:flex flex-col gap-9 p-10 border-r border-white/10">
|
|
<div className="flex items-center gap-3">
|
|
<ZinoLogo className="h-9 w-auto" />
|
|
<div className="text-xs text-on-navy-muted border-l border-white/15 pl-3">Lead-to-policy console</div>
|
|
</div>
|
|
|
|
<h1 className="m-0 text-[34px] leading-[1.12] font-extrabold tracking-[-0.025em] text-on-navy">
|
|
The AI-native
|
|
<br />
|
|
lead-to-policy console.
|
|
</h1>
|
|
|
|
<div className="flex flex-col gap-5 mt-auto">
|
|
{HIGHLIGHTS.map((h) => (
|
|
<div key={h.title} className="flex items-start gap-3.5">
|
|
<span className="w-9 h-9 shrink-0 rounded-[10px] bg-white/10 border border-white/10 flex items-center justify-center text-sunrise-300">
|
|
<h.Icon size={18} />
|
|
</span>
|
|
<div className="min-w-0">
|
|
<div className="text-sm font-semibold text-on-navy">{h.title}</div>
|
|
<div className="text-xs text-on-navy-muted mt-0.5">{h.body}</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* form side */}
|
|
<div className="p-8 lg:p-10 flex flex-col gap-7 justify-center">
|
|
<div className="flex flex-col items-center gap-3 text-center lg:items-start lg:text-left">
|
|
<span className="w-12 h-12 rounded-2xl bg-sunrise shadow-sunrise flex items-center justify-center text-white font-extrabold text-2xl font-numeric lg:hidden">
|
|
A
|
|
</span>
|
|
<div>
|
|
<div className="text-xl font-extrabold tracking-[-0.02em] text-on-navy">Welcome back</div>
|
|
<div className="text-sm text-on-navy-muted mt-0.5">Sign in to your console</div>
|
|
</div>
|
|
</div>
|
|
|
|
<form onSubmit={onSubmit} className="flex flex-col gap-4">
|
|
<Field
|
|
label="Email"
|
|
icon={<Mail size={16} />}
|
|
type="email"
|
|
value={email}
|
|
onChange={(e) => setEmail(e.target.value)}
|
|
autoComplete="username"
|
|
placeholder="you@company.com"
|
|
/>
|
|
<Field
|
|
label="Password"
|
|
icon={<Lock size={16} />}
|
|
type={show ? 'text' : 'password'}
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
autoComplete="current-password"
|
|
placeholder="••••••••"
|
|
trailing={
|
|
<button
|
|
type="button"
|
|
onClick={() => setShow((s) => !s)}
|
|
aria-label={show ? 'Hide password' : 'Show password'}
|
|
className="flex items-center justify-center text-on-navy-muted hover:text-on-navy cursor-pointer bg-transparent border-none p-0 shrink-0"
|
|
>
|
|
{show ? <EyeOff size={16} /> : <Eye size={16} />}
|
|
</button>
|
|
}
|
|
/>
|
|
{error && (
|
|
<div className="text-sm text-amber-200 bg-ruby-600/20 border border-ruby-400/30 rounded-md px-3 py-2">
|
|
{error}
|
|
</div>
|
|
)}
|
|
<Button variant="primary" full size="lg" disabled={busy} iconRight={!busy && <ArrowRight size={18} />}>
|
|
{busy ? 'Signing in…' : 'Sign in'}
|
|
</Button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|