lead-to-policy/src/pages/Login.tsx
Bhanu Prakash Sai Potteri a4d0f80f71 feat: Zino brand logo in sidebar + login, strip header search/bell
- 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>
2026-06-26 17:36:18 +05:30

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>
);
}