import { useState, useEffect, useRef } from 'react'; import { Eye, EyeOff, ArrowRight, Sun, Moon, Cpu, Shield, Zap, Lock, X, CheckCircle } from 'lucide-react'; import { fetchApi } from '../../api'; import { supabase } from '../../supabaseClient'; import { isPersonalEmailAllowed, PERSONAL_EMAIL_ERROR } from '../../utils/personalEmail'; const FONT_LINK = 'https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Syne:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&family=DM+Sans:opsz,wght@9..40,300;9..40,400;9..40,500;9..40,600&display=swap'; const KEYFRAMES = ` @keyframes lp-up { from{opacity:0;transform:translateY(18px)} to{opacity:1;transform:none} } @keyframes lp-left { from{opacity:0;transform:translateX(-18px)} to{opacity:1;transform:none} } @keyframes lp-glow { 0%,100%{opacity:.35;transform:scale(1)} 50%{opacity:.7;transform:scale(1.06)} } @keyframes lp-sway { 0%,100%{transform:rotate(-3deg) scale(1)} 50%{transform:rotate(3deg) scale(1.02)} } @keyframes lp-twinkle{ 0%,100%{opacity:.1;transform:scale(.7)} 50%{opacity:.85;transform:scale(1.3)} } @keyframes lp-orbit { from{transform:rotate(0deg) translateX(160px) rotate(0deg)} to{transform:rotate(360deg) translateX(160px) rotate(-360deg)} } @keyframes lp-shake { 0%,100%{transform:translateX(0)} 20%,60%{transform:translateX(-5px)} 40%,80%{transform:translateX(5px)} } @keyframes lp-spin { to{transform:rotate(360deg)} } @keyframes modal-in { from{opacity:0} to{opacity:1} } @keyframes modal-slide { from{opacity:0;transform:translateY(28px) scale(.97)} to{opacity:1;transform:none} } @keyframes success-pop { 0%{transform:scale(.5);opacity:0} 70%{transform:scale(1.15)} 100%{transform:scale(1);opacity:1} } @keyframes bar-fill { from{width:0%} to{width:100%} } .lp-form { animation: lp-up .5s cubic-bezier(.16,1,.3,1) forwards } .lp-panel { animation: lp-left .5s cubic-bezier(.16,1,.3,1) forwards } .lp-shake { animation: lp-shake .4s ease } .lp-spin { animation: lp-spin 1s linear infinite } .modal-in { animation: modal-in .22s ease forwards } .modal-slide{ animation: modal-slide .32s cubic-bezier(.16,1,.3,1) forwards } .success-pop{ animation: success-pop .45s cubic-bezier(.16,1,.3,1) forwards } .bar-fill { animation: bar-fill 2.4s linear forwards } `; const STARS = Array.from({ length: 70 }, (_, i) => ({ x: ((i * 137.508) % 100).toFixed(2), y: ((i * 97.3) % 100).toFixed(2), r: (0.5 + (i % 4) * 0.45).toFixed(1), d: ((i * 0.22) % 3).toFixed(2), t: (2 + (i % 5) * 0.7).toFixed(1), })); const LIGHT = { pageBg: '#ffffff', panelBg: '#111827', panelText: '#f9fafb', panelSub: 'rgba(249,250,251,.5)', panelAccent: '#0d9488', panelGrad: 'linear-gradient(135deg,#0d9488,#14b8a6)', cardBg: '#ffffff', cardBorder: 'rgba(209,213,219,.6)', cardShadow: '0 20px 60px rgba(0,0,0,.07)', t1: '#111827', t2: '#6b7280', t3: '#9ca3af', t4: 'rgba(209,213,219,.4)', acc: '#0d9488', acc2: '#14b8a6', accGrad: 'linear-gradient(135deg,#0d9488,#14b8a6)', pillBg: 'rgba(204,251,241,.5)', pillBorder: 'rgba(153,246,228,.8)', pillText: '#0f766e', inputBg: '#f9fafb', inputBorder: '#e5e7eb', inputFocus: '#0d9488', inputText: '#111827', inputPH: '#9ca3af', inputIcon: '#9ca3af', labelColor: '#6b7280', linkColor: '#0d9488', btnBg: 'linear-gradient(135deg,#0d9488,#14b8a6)', btnShadow: '0 6px 24px rgba(13,148,136,.3)', btnText: '#ffffff', dividerBg: 'rgba(209,213,219,.6)', dividerText: '#9ca3af', socialBg: '#f9fafb', socialBorder: '#e5e7eb', socialText: '#6b7280', footerText: '#d1d5db', toggleBg: '#f3f4f6', toggleBorder: '#e5e7eb', errorBg: '#fff5f5', errorBorder: '#fecaca', errorText: '#dc2626', modalOverlay: 'rgba(0,0,0,.45)', modalBg: '#ffffff', modalBorder: 'rgba(209,213,219,.6)', modalShadow: '0 32px 80px rgba(0,0,0,.18)', strengthTrack: '#e5e7eb', successIcon: '#0d9488', successSub: '#6b7280', }; const DARK = { pageBg: '#0c0908', panelBg: '#080605', panelText: '#ecfeff', panelSub: 'rgba(207,250,254,.45)', panelAccent: '#22d3ee', panelGrad: 'linear-gradient(135deg,#0e7490,#22d3ee)', cardBg: '#15100d', cardBorder: 'rgba(21,94,117,.3)', cardShadow: '0 20px 60px rgba(0,0,0,.7)', t1: '#ecfeff', t2: 'rgba(207,250,254,.6)', t3: 'rgba(207,250,254,.35)', t4: 'rgba(21,94,117,.2)', acc: '#22d3ee', acc2: '#a5f3fc', accGrad: 'linear-gradient(135deg,#0e7490,#22d3ee)', pillBg: 'rgba(21,94,117,.18)', pillBorder: 'rgba(21,94,117,.4)', pillText: '#a5f3fc', inputBg: '#080605', inputBorder: 'rgba(35,26,21,.8)', inputFocus: '#22d3ee', inputText: '#ecfeff', inputPH: 'rgba(207,250,254,.25)', inputIcon: 'rgba(34,211,238,.4)', labelColor: '#a5f3fc', linkColor: '#67e8f9', btnBg: '#e0f2fe', btnShadow: '0 6px 24px rgba(34,211,238,.2)', btnText: '#0c0908', dividerBg: 'rgba(35,26,21,.8)', dividerText: 'rgba(207,250,254,.35)', socialBg: 'rgba(21,94,117,.08)', socialBorder: 'rgba(35,26,21,.8)', socialText: '#a5f3fc', footerText: 'rgba(21,94,117,.5)', toggleBg: '#1a1310', toggleBorder: '#2a1f1a', errorBg: 'rgba(239,68,68,.08)', errorBorder: 'rgba(239,68,68,.25)', errorText: '#fca5a5', modalOverlay: 'rgba(0,0,0,.72)', modalBg: '#15100d', modalBorder: 'rgba(21,94,117,.35)', modalShadow: '0 32px 80px rgba(0,0,0,.8)', strengthTrack: 'rgba(35,26,21,.8)', successIcon: '#22d3ee', successSub: 'rgba(207,250,254,.6)', }; // ── defined only ONCE ── function getStrength(pw) { if (!pw) return { score: 0, label: '', color: 'transparent' }; let s = 0; if (pw.length >= 8) s++; if (/[A-Z]/.test(pw)) s++; if (/[0-9]/.test(pw)) s++; if (/[^A-Za-z0-9]/.test(pw)) s++; const map = [ { label: 'Too short', color: '#ef4444' }, { label: 'Weak', color: '#f97316' }, { label: 'Fair', color: '#eab308' }, { label: 'Strong', color: '#22c55e' }, { label: 'Very strong', color: '#14b8a6' }, ]; return { score: s, ...map[s] }; } const GoogleIcon = () => ( ); /* ══ FORGOT PASSWORD MODAL ════════════════════════════════════════════ */ function ForgotModal({ dark, T, onClose }) { const [step, setStep] = useState('email'); const [email, setEmail] = useState(''); const [code, setCode] = useState(''); const [newPw, setNewPw] = useState(''); const [confirm, setConfirm] = useState(''); const [showPw, setShowPw] = useState(false); const [showCf, setShowCf] = useState(false); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); const [shake, setShake] = useState(false); const overlayRef = useRef(null); const mono = { fontFamily: "'JetBrains Mono',monospace" }; const bebas = { fontFamily: "'Bebas Neue',cursive" }; const dm = { fontFamily: "'DM Sans',sans-serif" }; const strength = getStrength(newPw); const confirmMatch = confirm.length > 0 && newPw === confirm; const confirmBad = confirm.length > 0 && newPw !== confirm; const base = { ...dm, width: '100%', fontSize: 13, borderRadius: 12, outline: 'none', boxSizing: 'border-box', background: T.inputBg, border: `1px solid ${T.inputBorder}`, color: T.inputText, transition: 'border-color .2s' }; const triggerError = (msg) => { setError(msg); setShake(true); setTimeout(() => setShake(false), 450); }; const sendCode = async (e) => { e.preventDefault(); if (!email.trim()) return triggerError('Please enter your email.'); setError(''); setLoading(true); try { // TODO: replace with → POST /api/auth/check-email then supabase.auth.resetPasswordForEmail() await new Promise(r => setTimeout(r, 1000)); setStep('code'); } catch (err) { triggerError(err.message || 'Failed to send reset code. Please try again.'); } finally { setLoading(false); } }; const verifyCode = async (e) => { e.preventDefault(); if (code.length < 4) return triggerError('Enter the 6-digit code sent to your email.'); setError(''); setLoading(true); await new Promise(r => setTimeout(r, 800)); setLoading(false); setStep('reset'); }; const resetPassword = async (e) => { e.preventDefault(); if (newPw.length < 8) return triggerError('Password must be at least 8 characters.'); if (newPw !== confirm) return triggerError('Passwords do not match.'); setError(''); setLoading(true); await new Promise(r => setTimeout(r, 1200)); setLoading(false); setStep('done'); setTimeout(onClose, 2200); }; const stepProgress = { email: 0, code: 1, reset: 2, done: 3 }; const stepLabel = { email: 'FORGOT PASSWORD', code: 'CHECK YOUR EMAIL', reset: 'NEW PASSWORD', done: 'ALL DONE' }; const stepTitle = { email: 'Reset Your\nPassword', code: 'Enter Your\nVerification\nCode', reset: 'Create New\nPassword', done: 'Password\nUpdated!' }; const ForgotBtn = ({ lbl }) => ( ); return (
{ if (e.target === overlayRef.current) onClose(); }} style={{ position: 'fixed', inset: 0, zIndex: 1000, background: T.modalOverlay, backdropFilter: 'blur(7px)', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 16 }}>
{[0, 1, 2].map(i => (
= i ? T.acc : T.strengthTrack, transition: 'background .3s' }} /> ))}
{stepLabel[step]}

{stepTitle[step]}

{error && (
{error}
)} {step === 'email' && (
setEmail(e.target.value)} placeholder="you@company.com" autoComplete="email" style={{ ...base, padding: '11px 14px' }} onFocus={e => e.target.style.borderColor = T.inputFocus} onBlur={e => e.target.style.borderColor = T.inputBorder} />

We'll send a 6-digit code to this address.

)} {step === 'code' && (

Code sent to {email}

setCode(e.target.value.replace(/\D/g, '').slice(0, 6))} placeholder="000000" autoComplete="one-time-code" style={{ ...base, padding: '11px 14px', letterSpacing: '8px', fontSize: 20, textAlign: 'center' }} onFocus={e => e.target.style.borderColor = T.inputFocus} onBlur={e => e.target.style.borderColor = T.inputBorder} />
)} {step === 'reset' && (
setNewPw(e.target.value)} placeholder="Min. 8 characters" autoComplete="new-password" style={{ ...base, padding: '11px 42px 11px 14px' }} onFocus={e => e.target.style.borderColor = T.inputFocus} onBlur={e => e.target.style.borderColor = T.inputBorder} />
{newPw && (
{[1, 2, 3, 4].map(i => (
))}
= 3 ? strength.color : T.t3 }}>{strength.label}
)}
setConfirm(e.target.value)} placeholder="Repeat new password" autoComplete="new-password" style={{ ...base, padding: '11px 42px 11px 14px', border: `1px solid ${confirmBad ? T.errorBorder : confirmMatch ? T.acc : T.inputBorder}`, background: confirmBad ? (dark ? 'rgba(239,68,68,.05)' : '#fff5f5') : T.inputBg }} onFocus={e => { if (!confirmBad) e.target.style.borderColor = T.inputFocus; }} onBlur={e => { e.target.style.borderColor = confirmBad ? T.errorBorder : confirmMatch ? T.acc : T.inputBorder; }} />
{confirmBad &&

Passwords don't match

}
)} {step === 'done' && (

Your password has been updated.
You can now sign in.

)}
); } /* ══ SIGNUP MODAL ═════════════════════════════════════════════════════ */ function SignupModal({ dark, T, onClose, onSuccess }) { const [name, setName] = useState(''); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [confirm, setConfirm] = useState(''); const [showPw, setShowPw] = useState(false); const [showCf, setShowCf] = useState(false); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); const [shake, setShake] = useState(false); const [done, setDone] = useState(false); const overlayRef = useRef(null); const mono = { fontFamily: "'JetBrains Mono',monospace" }; const bebas = { fontFamily: "'Bebas Neue',cursive" }; const syne = { fontFamily: "'Syne',sans-serif" }; const dm = { fontFamily: "'DM Sans',sans-serif" }; const strength = getStrength(password); const confirmMatch = confirm.length > 0 && password === confirm; const confirmBad = confirm.length > 0 && password !== confirm; const base = { ...dm, width: '100%', fontSize: 13, borderRadius: 12, outline: 'none', boxSizing: 'border-box', background: T.inputBg, border: `1px solid ${T.inputBorder}`, color: T.inputText, transition: 'border-color .2s', }; const triggerError = (msg) => { setError(msg); setShake(true); setTimeout(() => setShake(false), 450); }; // ── backend's real signup via fetchApi ── const handleSubmit = async (e) => { e.preventDefault(); if (!name.trim()) return triggerError('Please enter your name.'); if (!email.trim()) return triggerError('Please enter your email.'); if (!isPersonalEmailAllowed(email)) { localStorage.removeItem('token'); return triggerError(PERSONAL_EMAIL_ERROR); } if (password.length < 8) return triggerError('Password must be at least 8 characters.'); if (password !== confirm) return triggerError('Passwords do not match.'); setError(''); setLoading(true); try { const data = await fetchApi('/auth/signup', { method: 'POST', body: { name, email, password } }); console.log('Signup successful:', data); localStorage.setItem('token', data.token); setDone(true); setTimeout(() => { onSuccess?.({ name, email }); onClose(); }, 2000); } catch (err) { triggerError(err.message || 'Registration failed. Please try again.'); } finally { setLoading(false); } }; return (
{ if (e.target === overlayRef.current) onClose(); }} style={{ position: 'fixed', inset: 0, zIndex: 999, background: T.modalOverlay, backdropFilter: 'blur(7px)', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 16, }} >
{done ? (

Account Created!

Welcome, {name}!
Taking you to upload your first document…

) : ( <>
PolicyLens
FREE ACCOUNT

Create Your Account

No credit card required · 14-day full access

{error && (
{error}
)}
{/* NAME */}
setName(e.target.value)} placeholder="Ada Lovelace" autoComplete="name" style={{ ...base, padding: '11px 14px' }} onFocus={e => e.target.style.borderColor = T.inputFocus} onBlur={e => e.target.style.borderColor = T.inputBorder} />
{/* EMAIL */}
setEmail(e.target.value)} placeholder="you@company.com" autoComplete="email" style={{ ...base, padding: '11px 14px' }} onFocus={e => e.target.style.borderColor = T.inputFocus} onBlur={e => e.target.style.borderColor = T.inputBorder} />
{/* PASSWORD */}
setPassword(e.target.value)} placeholder="Min. 8 characters" autoComplete="new-password" style={{ ...base, padding: '11px 42px 11px 14px' }} onFocus={e => e.target.style.borderColor = T.inputFocus} onBlur={e => e.target.style.borderColor = T.inputBorder} />
{password && (
{[1, 2, 3, 4].map(i => (
))}
= 3 ? strength.color : T.t3 }}> {strength.label}
)}
{/* CONFIRM PASSWORD */}
setConfirm(e.target.value)} placeholder="Repeat your password" autoComplete="new-password" style={{ ...base, padding: '11px 42px 11px 14px', border: `1px solid ${confirmBad ? T.errorBorder : confirmMatch ? T.acc : T.inputBorder}`, background: confirmBad ? (dark ? 'rgba(239,68,68,.05)' : '#fff5f5') : T.inputBg, }} onFocus={e => { if (!confirmBad) e.target.style.borderColor = T.inputFocus; }} onBlur={e => { e.target.style.borderColor = confirmBad ? T.errorBorder : confirmMatch ? T.acc : T.inputBorder; }} />
{confirmBad && (

Passwords don't match

)}

Already have an account?{' '}

Secured by PolicyLens · SOC 2 Compliant

)}
); } /* ══ LOGIN PAGE ═══════════════════════════════════════════════════════ */ export default function LoginPage({ onLoginSuccess, onGoogleSuccess, onSignupSuccess, forcedError = '' }) { const [dark, setDark] = useState(false); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [showPw, setShowPw] = useState(false); const [loading, setLoading] = useState(false); const [googleLoading, setGoogleLoading] = useState(false); const [error, setError] = useState(''); const [shake, setShake] = useState(false); const [showSignup, setShowSignup] = useState(false); const [showForgot, setShowForgot] = useState(false); const T = dark ? DARK : LIGHT; useEffect(() => { const a = Object.assign(document.createElement('link'), { rel: 'stylesheet', href: FONT_LINK }); const b = Object.assign(document.createElement('style'), { textContent: KEYFRAMES }); document.head.append(a, b); return () => { a.remove(); b.remove(); }; }, []); useEffect(() => { document.body.style.overflow = (showSignup || showForgot) ? 'hidden' : ''; return () => { document.body.style.overflow = ''; }; }, [showSignup, showForgot]); useEffect(() => { if (forcedError) { setError(forcedError); } }, [forcedError]); const deriveName = (e) => e.split('@')[0].replace(/[._-]+/g, ' ').replace(/\b\w/g, c => c.toUpperCase()).trim() || 'User'; const triggerError = (msg) => { setError(msg); setShake(true); setTimeout(() => setShake(false), 450); }; const handleSubmit = async (e) => { e.preventDefault(); if (!email.trim()) return triggerError('Please enter your email.'); if (!isPersonalEmailAllowed(email)) { localStorage.removeItem('token'); return triggerError(PERSONAL_EMAIL_ERROR); } if (!password.trim()) return triggerError('Please enter your password.'); setError(''); setLoading(true); try { const data = await fetchApi('/auth/login', { method: 'POST', body: { email, password } }); localStorage.setItem('token', data.token); console.log('Login successful:', data); onLoginSuccess?.({ email, name: data.user?.name || deriveName(email) }); } catch (err) { triggerError(err.message || 'Invalid credentials. Please try again.'); } finally { setLoading(false); } }; const handleGoogle = async () => { setError(''); setGoogleLoading(true); try { localStorage.setItem('oauth_redirect_intent', 'google'); const redirectTo = `${window.location.origin}${window.location.pathname}`; const { data, error: oauthError } = await supabase.auth.signInWithOAuth({ provider: 'google', options: { redirectTo, skipBrowserRedirect: true, queryParams: { prompt: 'select_account', }, }, }); if (oauthError) throw oauthError; if (!data?.url) throw new Error('Unable to start Google sign-in flow.'); window.location.assign(data.url); } catch (err) { localStorage.removeItem('oauth_redirect_intent'); triggerError(err.message || 'Google login failed. Please try again.'); setGoogleLoading(false); } }; const f = { fontFamily: "'DM Sans',sans-serif" }; const mono = { fontFamily: "'JetBrains Mono',monospace" }; const bebas = { fontFamily: "'Bebas Neue',cursive" }; const syne = { fontFamily: "'Syne',sans-serif" }; return (
{/* ── LEFT PANEL ── */}
{dark ? (
{STARS.map((s, i) => ( ))}
) : (
)}
PolicyLens

Your Legal
Intelligence
Platform

Upload contracts and policies. Our AI scans every clause, flags every risk, and answers every question.

{[ { icon: Shield, text: 'End-to-end encrypted analysis' }, { icon: Zap, text: 'Risk detection in under 30 seconds' }, { icon: Lock, text: 'Zero data retention policy' }, ].map(({ icon: Icon, text }) => (
{text}
))}
{/* ── RIGHT: LOGIN FORM ── */}
SECURE LOGIN

Welcome

Sign in to continue to your dashboard.

{error && (
{error}
)}
setEmail(e.target.value)} placeholder="you@company.com" autoComplete="email" style={{ ...f, width: '100%', fontSize: 13, padding: '11px 14px', borderRadius: 12, outline: 'none', boxSizing: 'border-box', background: T.inputBg, border: `1px solid ${T.inputBorder}`, color: T.inputText, transition: 'border-color .2s, background .4s', }} onFocus={e => e.target.style.borderColor = T.inputFocus} onBlur={e => e.target.style.borderColor = T.inputBorder} />
setPassword(e.target.value)} placeholder="••••••••••" autoComplete="current-password" style={{ ...f, width: '100%', fontSize: 13, padding: '11px 44px 11px 14px', borderRadius: 12, outline: 'none', boxSizing: 'border-box', background: T.inputBg, border: `1px solid ${T.inputBorder}`, color: T.inputText, transition: 'border-color .2s, background .4s', }} onFocus={e => e.target.style.borderColor = T.inputFocus} onBlur={e => e.target.style.borderColor = T.inputBorder} />
OR

Don't have an account?{' '}

Secured by PolicyLens · v2.4.1 · SOC 2 Compliant

{showSignup && ( setShowSignup(false)} onSuccess={(user) => { setShowSignup(false); onSignupSuccess?.(user); }} /> )} {showForgot && ( setShowForgot(false)} /> )}
); }