devjhawar's picture
Upload folder using huggingface_hub
dfd845a verified
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 = () => (
<svg width="16" height="16" viewBox="0 0 24 24">
<path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" />
<path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" />
<path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l3.66-2.84z" />
<path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" />
</svg>
);
/* ══ 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 }) => (
<button type="submit" disabled={loading} style={{
fontFamily: "'Syne',sans-serif", width: '100%', padding: '12px', borderRadius: 13, border: 'none',
fontSize: 13, fontWeight: 600, cursor: loading ? 'not-allowed' : 'pointer',
background: loading ? T.t4 : T.btnBg, color: T.btnText,
boxShadow: loading ? 'none' : T.btnShadow,
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8,
transition: 'all .2s', opacity: loading ? .7 : 1
}}
onMouseEnter={e => { if (!loading) e.currentTarget.style.transform = 'translateY(-1px)'; }}
onMouseLeave={e => e.currentTarget.style.transform = 'none'}>
{loading
? <><span className="lp-spin" style={{
display: 'inline-block', width: 14, height: 14,
border: `2px solid rgba(0,0,0,.18)`, borderTopColor: dark ? '#0c0908' : '#fff',
borderRadius: '50%'
}} /> Please wait…</>
: <>{lbl} <ArrowRight size={14} /></>}
</button>
);
return (
<div ref={overlayRef} className="modal-in"
onClick={e => { 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
}}>
<div className="modal-slide" style={{
position: 'relative', width: 'min(420px,100%)', borderRadius: 24,
background: T.modalBg, border: `1px solid ${T.modalBorder}`, boxShadow: T.modalShadow,
padding: '38px 40px 34px'
}}>
<button onClick={onClose} style={{
position: 'absolute', top: 14, right: 14, width: 30, height: 30, borderRadius: 8, cursor: 'pointer',
display: 'flex', alignItems: 'center', justifyContent: 'center',
background: 'transparent', border: `1px solid ${T.inputBorder}`, color: T.t3, transition: 'all .15s'
}}
onMouseEnter={e => { e.currentTarget.style.background = T.inputBg; e.currentTarget.style.color = T.t1; }}
onMouseLeave={e => { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.color = T.t3; }}>
<X size={13} />
</button>
<div style={{ display: 'flex', gap: 5, marginBottom: 22 }}>
{[0, 1, 2].map(i => (
<div key={i} style={{
flex: 1, height: 3, borderRadius: 99,
background: stepProgress[step] >= i ? T.acc : T.strengthTrack, transition: 'background .3s'
}} />
))}
</div>
<span style={{
...mono, display: 'inline-block', marginBottom: 10, fontSize: 9,
letterSpacing: '3.5px', padding: '3px 12px', borderRadius: 99,
background: T.pillBg, border: `1px solid ${T.pillBorder}`, color: T.pillText
}}>
{stepLabel[step]}
</span>
<h2 style={{
...bebas, fontSize: 30, letterSpacing: 2, lineHeight: 1.1, color: T.t1,
margin: '0 0 20px', whiteSpace: 'pre-line'
}}>
{stepTitle[step]}
</h2>
{error && (
<div className={shake ? 'lp-shake' : ''} style={{
marginBottom: 14, padding: '9px 13px', borderRadius: 10, fontSize: 12, ...dm,
background: T.errorBg, border: `1px solid ${T.errorBorder}`, color: T.errorText,
display: 'flex', alignItems: 'center', gap: 8
}}>
<span></span> {error}
</div>
)}
{step === 'email' && (
<form onSubmit={sendCode} style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
<div>
<label style={{ ...mono, display: 'block', fontSize: 10, letterSpacing: '1.5px', marginBottom: 6, color: T.labelColor }}>
EMAIL ADDRESS
</label>
<input type="email" value={email} onChange={e => 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} />
</div>
<p style={{ ...dm, fontSize: 12, color: T.t2, margin: 0 }}>We'll send a 6-digit code to this address.</p>
<ForgotBtn lbl="Send Reset Code" />
</form>
)}
{step === 'code' && (
<form onSubmit={verifyCode} style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
<p style={{ ...dm, fontSize: 12, color: T.t2, margin: '0 0 4px' }}>
Code sent to <strong style={{ color: T.t1 }}>{email}</strong>
</p>
<div>
<label style={{ ...mono, display: 'block', fontSize: 10, letterSpacing: '1.5px', marginBottom: 6, color: T.labelColor }}>
6-DIGIT CODE
</label>
<input type="text" value={code}
onChange={e => 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} />
</div>
<ForgotBtn lbl="Verify Code" />
<button type="button" onClick={() => { setStep('email'); setCode(''); }} style={{
...dm, fontSize: 12, color: T.linkColor, background: 'none', border: 'none',
cursor: 'pointer', padding: 0, textAlign: 'center'
}}>
← Use a different email
</button>
</form>
)}
{step === 'reset' && (
<form onSubmit={resetPassword} style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
<div>
<label style={{ ...mono, display: 'block', fontSize: 10, letterSpacing: '1.5px', marginBottom: 6, color: T.labelColor }}>
NEW PASSWORD
</label>
<div style={{ position: 'relative' }}>
<input type={showPw ? 'text' : 'password'} value={newPw} onChange={e => 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} />
<button type="button" onClick={() => setShowPw(v => !v)} style={{
position: 'absolute', right: 12, top: '50%', transform: 'translateY(-50%)',
background: 'none', border: 'none', cursor: 'pointer', padding: 0,
color: T.inputIcon, display: 'flex', alignItems: 'center'
}}>
{showPw ? <EyeOff size={14} /> : <Eye size={14} />}
</button>
</div>
{newPw && (
<div style={{ marginTop: 7 }}>
<div style={{ display: 'flex', gap: 3, marginBottom: 3 }}>
{[1, 2, 3, 4].map(i => (
<div key={i} style={{
flex: 1, height: 3, borderRadius: 99,
background: i <= strength.score ? strength.color : T.strengthTrack, transition: 'background .25s'
}} />
))}
</div>
<span style={{ ...mono, fontSize: 10, color: strength.score >= 3 ? strength.color : T.t3 }}>{strength.label}</span>
</div>
)}
</div>
<div>
<label style={{ ...mono, display: 'block', fontSize: 10, letterSpacing: '1.5px', marginBottom: 6, color: T.labelColor }}>
CONFIRM PASSWORD
</label>
<div style={{ position: 'relative' }}>
<input type={showCf ? 'text' : 'password'} value={confirm} onChange={e => 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; }} />
<button type="button" onClick={() => setShowCf(v => !v)} style={{
position: 'absolute', right: 12, top: '50%', transform: 'translateY(-50%)',
background: 'none', border: 'none', cursor: 'pointer', padding: 0,
color: T.inputIcon, display: 'flex', alignItems: 'center'
}}>
{showCf ? <EyeOff size={14} /> : <Eye size={14} />}
</button>
</div>
{confirmBad && <p style={{ ...mono, marginTop: 4, fontSize: 10, color: T.errorText }}>Passwords don't match</p>}
</div>
<ForgotBtn lbl="Save New Password" />
</form>
)}
{step === 'done' && (
<div style={{ textAlign: 'center', padding: '8px 0 4px' }}>
<div className="success-pop" style={{
width: 60, height: 60, borderRadius: '50%', margin: '0 auto 18px',
background: dark ? 'rgba(34,211,238,.08)' : 'rgba(204,251,241,.5)',
border: `1px solid ${dark ? 'rgba(34,211,238,.2)' : 'rgba(13,148,136,.25)'}`,
display: 'flex', alignItems: 'center', justifyContent: 'center'
}}>
<CheckCircle size={26} style={{ color: T.successIcon }} />
</div>
<p style={{ ...dm, fontSize: 13, color: T.successSub, lineHeight: 1.7 }}>
Your password has been updated.<br />You can now sign in.
</p>
</div>
)}
</div>
</div>
);
}
/* ══ 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 (
<div
ref={overlayRef}
className="modal-in"
onClick={e => { 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,
}}
>
<div className="modal-slide" style={{
position: 'relative', width: 'min(450px,100%)',
maxHeight: '92vh', overflowY: 'auto',
borderRadius: 24, background: T.modalBg,
border: `1px solid ${T.modalBorder}`, boxShadow: T.modalShadow,
padding: '38px 40px 34px',
}}>
<button onClick={onClose} style={{
position: 'absolute', top: 14, right: 14, width: 30, height: 30,
borderRadius: 8, cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center',
background: 'transparent', border: `1px solid ${T.inputBorder}`,
color: T.t3, transition: 'all .15s',
}}
onMouseEnter={e => { e.currentTarget.style.background = T.inputBg; e.currentTarget.style.color = T.t1; }}
onMouseLeave={e => { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.color = T.t3; }}>
<X size={13} />
</button>
{done ? (
<div style={{ textAlign: 'center', padding: '20px 0 8px' }}>
<div className="success-pop" style={{
width: 64, height: 64, borderRadius: '50%', margin: '0 auto 20px',
background: dark ? 'rgba(34,211,238,.08)' : 'rgba(204,251,241,.5)',
border: `1px solid ${dark ? 'rgba(34,211,238,.2)' : 'rgba(13,148,136,.25)'}`,
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}>
<CheckCircle size={28} style={{ color: T.successIcon }} />
</div>
<h2 style={{ ...bebas, fontSize: 30, letterSpacing: 2, color: T.t1, margin: '0 0 8px' }}>
Account Created!
</h2>
<p style={{ ...dm, fontSize: 13, color: T.successSub, lineHeight: 1.7, maxWidth: 280, margin: '0 auto' }}>
Welcome, <strong style={{ color: T.t1 }}>{name}</strong>!<br />
Taking you to upload your first document…
</p>
<div style={{ marginTop: 24, height: 3, borderRadius: 99, background: T.strengthTrack, overflow: 'hidden' }}>
<div className="bar-fill" style={{ height: '100%', borderRadius: 99, background: T.accGrad }} />
</div>
</div>
) : (
<>
<div style={{ marginBottom: 24 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 9, marginBottom: 12 }}>
<div style={{
width: 34, height: 34, borderRadius: 10,
background: T.panelGrad, display: 'flex', alignItems: 'center', justifyContent: 'center',
boxShadow: `0 4px 12px ${T.acc}44`
}}>
<Cpu size={15} color="#fff" />
</div>
<span style={{ ...bebas, fontSize: 17, letterSpacing: 3, color: T.acc }}>PolicyLens</span>
</div>
<span style={{
...mono, display: 'inline-block', marginBottom: 10, fontSize: 9,
letterSpacing: '3.5px', padding: '3px 12px', borderRadius: 99,
background: T.pillBg, border: `1px solid ${T.pillBorder}`, color: T.pillText,
}}>FREE ACCOUNT</span>
<h2 style={{ ...bebas, fontSize: 32, letterSpacing: 2, lineHeight: 1.05, color: T.t1, margin: '0 0 5px' }}>
Create Your Account
</h2>
<p style={{ ...dm, fontSize: 12, color: T.t2, margin: 0 }}>
No credit card required · 14-day full access
</p>
</div>
{error && (
<div className={shake ? 'lp-shake' : ''} style={{
marginBottom: 16, padding: '9px 13px', borderRadius: 10, fontSize: 12,
background: T.errorBg, border: `1px solid ${T.errorBorder}`, color: T.errorText,
display: 'flex', alignItems: 'center', gap: 8, ...dm,
}}>
<span style={{ fontSize: 14 }}></span> {error}
</div>
)}
<form onSubmit={handleSubmit} style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
{/* NAME */}
<div>
<label style={{
...mono, display: 'block', fontSize: 10, fontWeight: 500,
letterSpacing: '1.5px', marginBottom: 6, color: T.labelColor
}}>
FULL NAME
</label>
<input
type="text"
value={name}
onChange={e => 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}
/>
</div>
{/* EMAIL */}
<div>
<label style={{
...mono, display: 'block', fontSize: 10, fontWeight: 500,
letterSpacing: '1.5px', marginBottom: 6, color: T.labelColor
}}>
EMAIL ADDRESS
</label>
<input
type="email"
value={email}
onChange={e => 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}
/>
</div>
{/* PASSWORD */}
<div>
<label style={{
...mono, display: 'block', fontSize: 10, fontWeight: 500,
letterSpacing: '1.5px', marginBottom: 6, color: T.labelColor
}}>
PASSWORD
</label>
<div style={{ position: 'relative' }}>
<input
type={showPw ? 'text' : 'password'}
value={password}
onChange={e => 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}
/>
<button type="button" onClick={() => setShowPw(v => !v)} style={{
position: 'absolute', right: 12, top: '50%', transform: 'translateY(-50%)',
background: 'none', border: 'none', cursor: 'pointer', padding: 0,
color: T.inputIcon, display: 'flex', alignItems: 'center',
}}>
{showPw ? <EyeOff size={14} /> : <Eye size={14} />}
</button>
</div>
{password && (
<div style={{ marginTop: 7 }}>
<div style={{ display: 'flex', gap: 3, marginBottom: 3 }}>
{[1, 2, 3, 4].map(i => (
<div key={i} style={{
flex: 1, height: 3, borderRadius: 99,
background: i <= strength.score ? strength.color : T.strengthTrack,
transition: 'background .25s',
}} />
))}
</div>
<span style={{ ...mono, fontSize: 10, color: strength.score >= 3 ? strength.color : T.t3 }}>
{strength.label}
</span>
</div>
)}
</div>
{/* CONFIRM PASSWORD */}
<div>
<label style={{
...mono, display: 'block', fontSize: 10, fontWeight: 500,
letterSpacing: '1.5px', marginBottom: 6, color: T.labelColor
}}>
CONFIRM PASSWORD
</label>
<div style={{ position: 'relative' }}>
<input
type={showCf ? 'text' : 'password'}
value={confirm}
onChange={e => 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; }}
/>
<button type="button" onClick={() => setShowCf(v => !v)} style={{
position: 'absolute', right: 12, top: '50%', transform: 'translateY(-50%)',
background: 'none', border: 'none', cursor: 'pointer', padding: 0,
color: T.inputIcon, display: 'flex', alignItems: 'center',
}}>
{showCf ? <EyeOff size={14} /> : <Eye size={14} />}
</button>
</div>
{confirmBad && (
<p style={{ ...mono, marginTop: 4, fontSize: 10, color: T.errorText }}>
Passwords don't match
</p>
)}
</div>
<button type="submit" disabled={loading} style={{
...syne, width: '100%', padding: '13px', borderRadius: 13, border: 'none',
fontSize: 14, fontWeight: 600, cursor: loading ? 'not-allowed' : 'pointer',
background: loading ? T.t4 : T.btnBg, color: T.btnText,
boxShadow: loading ? 'none' : T.btnShadow,
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8,
transition: 'all .2s', opacity: loading ? .7 : 1, marginTop: 6,
}}
onMouseEnter={e => { if (!loading) e.currentTarget.style.transform = 'translateY(-1px)'; }}
onMouseLeave={e => e.currentTarget.style.transform = 'none'}>
{loading
? <><span className="lp-spin" style={{
display: 'inline-block', width: 15, height: 15,
border: `2px solid rgba(0,0,0,.18)`, borderTopColor: dark ? '#0c0908' : '#fff',
borderRadius: '50%'
}} /> Creating account…</>
: <>Create Account <ArrowRight size={15} /></>
}
</button>
</form>
<p style={{ ...dm, marginTop: 18, textAlign: 'center', fontSize: 12, color: T.t2 }}>
Already have an account?{' '}
<button type="button" onClick={onClose} style={{
fontFamily: "'DM Sans',sans-serif", fontSize: 12, fontWeight: 600,
color: T.linkColor, background: 'none', border: 'none', cursor: 'pointer', padding: 0,
}}>
Sign in →
</button>
</p>
<p style={{
...mono, marginTop: 14, textAlign: 'center', fontSize: 10,
letterSpacing: .5, color: T.footerText
}}>
Secured by PolicyLens · SOC 2 Compliant
</p>
</>
)}
</div>
</div>
);
}
/* ══ 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 (
<div style={{
...f, minHeight: '100vh', background: T.pageBg, transition: 'background .4s',
display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 16
}}>
<button onClick={() => setDark(v => !v)} style={{
position: 'fixed', top: 20, right: 20, zIndex: 98,
width: 38, height: 38, borderRadius: 11, cursor: 'pointer',
display: 'flex', alignItems: 'center', justifyContent: 'center',
background: T.toggleBg, border: `1px solid ${T.toggleBorder}`, transition: 'all .2s',
}}
onMouseEnter={e => { e.currentTarget.style.transform = 'rotate(14deg) scale(1.1)'; }}
onMouseLeave={e => { e.currentTarget.style.transform = 'none'; }}>
{dark ? <Sun size={15} style={{ color: '#22d3ee' }} /> : <Moon size={15} style={{ color: T.acc }} />}
</button>
<div style={{
display: 'flex', overflow: 'hidden',
width: 'min(920px,100%)', minHeight: 580, borderRadius: 28,
background: T.cardBg, border: `1px solid ${T.cardBorder}`,
boxShadow: T.cardShadow, transition: 'background .4s, border-color .4s, box-shadow .4s',
}}>
{/* ── LEFT PANEL ── */}
<div className="lp-panel" style={{
width: 380, flexShrink: 0, position: 'relative', overflow: 'hidden',
background: T.panelBg, display: 'flex', flexDirection: 'column',
justifyContent: 'space-between', padding: 40,
}}>
{dark ? (
<div style={{ position: 'absolute', inset: 0, pointerEvents: 'none' }}>
<svg style={{ position: 'absolute', inset: 0, width: '100%', height: '100%' }}>
{STARS.map((s, i) => (
<circle key={i} cx={`${s.x}%`} cy={`${s.y}%`} r={s.r} fill="#a5f3fc"
style={{ animation: `lp-twinkle ${s.t}s ease-in-out infinite`, animationDelay: `${s.d}s`, opacity: .12 }} />
))}
</svg>
<div style={{
position: 'absolute', width: 480, height: 480, borderRadius: '50%',
top: '30%', left: '50%', transform: 'translate(-50%,-50%)', filter: 'blur(90px)',
background: 'radial-gradient(circle,rgba(34,211,238,.12) 0%,transparent 65%)',
animation: 'lp-glow 6s ease-in-out infinite'
}} />
<div style={{ position: 'absolute', top: '50%', left: '50%', width: 0, height: 0 }}>
<div style={{
position: 'absolute', width: 4, height: 4, borderRadius: '50%',
background: '#22d3ee', boxShadow: '0 0 8px #22d3ee', marginLeft: -2, marginTop: -2,
animation: 'lp-orbit 14s linear infinite', opacity: .5
}} />
</div>
<svg style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', opacity: .03 }}>
<defs><pattern id="lpg" x="0" y="0" width="50" height="50" patternUnits="userSpaceOnUse">
<path d="M50 0L0 0 0 50" fill="none" stroke="#22d3ee" strokeWidth=".6" />
</pattern></defs>
<rect width="100%" height="100%" fill="url(#lpg)" />
</svg>
</div>
) : (
<div style={{ position: 'absolute', inset: 0, pointerEvents: 'none' }}>
<svg style={{
position: 'absolute', top: '-8%', right: '-10%', width: 280, height: 280,
animation: 'lp-sway 8s ease-in-out infinite', opacity: .07
}}>
<ellipse cx="140" cy="140" rx="120" ry="55" fill="#14b8a6" transform="rotate(-28 140 140)" />
<ellipse cx="140" cy="140" rx="90" ry="38" fill="#0d9488" transform="rotate(12 140 140)" />
</svg>
<svg style={{
position: 'absolute', bottom: '-10%', left: '-8%', width: 220, height: 220,
animation: 'lp-sway 11s ease-in-out infinite', animationDelay: '2s', opacity: .06
}}>
<ellipse cx="110" cy="110" rx="95" ry="42" fill="#14b8a6" transform="rotate(22 110 110)" />
</svg>
<div style={{
position: 'absolute', width: 400, height: 400, borderRadius: '50%',
top: '50%', left: '50%', transform: 'translate(-50%,-50%)', filter: 'blur(80px)',
background: 'radial-gradient(circle,rgba(13,148,136,.12) 0%,transparent 65%)',
animation: 'lp-glow 7s ease-in-out infinite'
}} />
<svg style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', opacity: .06 }}>
<defs><pattern id="lpd" x="0" y="0" width="22" height="22" patternUnits="userSpaceOnUse">
<circle cx="1" cy="1" r=".9" fill="#0d9488" />
</pattern></defs>
<rect width="100%" height="100%" fill="url(#lpd)" />
</svg>
</div>
)}
<div style={{ position: 'relative', zIndex: 2 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 40 }}>
<div style={{
width: 40, height: 40, borderRadius: 12, background: T.panelGrad,
display: 'flex', alignItems: 'center', justifyContent: 'center',
boxShadow: `0 4px 16px ${T.panelAccent}55`
}}>
<Cpu size={18} color="#fff" />
</div>
<span style={{ ...bebas, fontSize: 22, letterSpacing: 3, color: T.panelText }}>PolicyLens</span>
</div>
<h2 style={{ ...bebas, fontSize: 44, letterSpacing: 2, lineHeight: 1.05, color: T.panelText, margin: '0 0 16px' }}>
Your Legal<br />Intelligence<br />Platform
</h2>
<p style={{ fontSize: 13, lineHeight: 1.7, color: T.panelSub, maxWidth: 260 }}>
Upload contracts and policies. Our AI scans every clause, flags every risk, and answers every question.
</p>
</div>
<div style={{ position: 'relative', zIndex: 2, display: 'flex', flexDirection: 'column', gap: 12 }}>
{[
{ 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 }) => (
<div key={text} style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<div style={{
width: 28, height: 28, borderRadius: 8, flexShrink: 0,
display: 'flex', alignItems: 'center', justifyContent: 'center',
background: dark ? 'rgba(34,211,238,.1)' : 'rgba(13,148,136,.15)',
border: `1px solid ${dark ? 'rgba(34,211,238,.2)' : 'rgba(13,148,136,.25)'}`
}}>
<Icon size={13} style={{ color: T.panelAccent }} />
</div>
<span style={{ fontSize: 12, color: T.panelSub }}>{text}</span>
</div>
))}
</div>
</div>
{/* ── RIGHT: LOGIN FORM ── */}
<div style={{
flex: 1, display: 'flex', flexDirection: 'column',
justifyContent: 'center', padding: '48px 52px'
}}>
<div className="lp-form">
<div style={{ marginBottom: 36 }}>
<span style={{
...mono, display: 'inline-block', marginBottom: 12,
fontSize: 9, letterSpacing: '3.5px', padding: '3px 12px', borderRadius: 99,
background: T.pillBg, border: `1px solid ${T.pillBorder}`, color: T.pillText,
}}>SECURE LOGIN</span>
<h1 style={{
...bebas, fontSize: 40, letterSpacing: 2, lineHeight: 1,
color: T.t1, margin: '0 0 8px', transition: 'color .4s'
}}>
Welcome
</h1>
<p style={{ fontSize: 13, color: T.t2, margin: 0 }}>
Sign in to continue to your dashboard.
</p>
</div>
{error && (
<div className={shake ? 'lp-shake' : ''} style={{
marginBottom: 20, padding: '10px 14px', borderRadius: 10, fontSize: 12,
background: T.errorBg, border: `1px solid ${T.errorBorder}`, color: T.errorText,
display: 'flex', alignItems: 'center', gap: 8,
}}>
<span style={{ fontSize: 15 }}></span> {error}
</div>
)}
<form onSubmit={handleSubmit} style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<div>
<label style={{
...f, display: 'block', fontSize: 11, fontWeight: 500,
letterSpacing: 1, marginBottom: 6, color: T.labelColor, ...mono
}}>
EMAIL ADDRESS
</label>
<input
type="email" value={email} onChange={e => 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}
/>
</div>
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 6 }}>
<label style={{
...f, display: 'block', fontSize: 11, fontWeight: 500,
letterSpacing: 1, color: T.labelColor, ...mono
}}>PASSWORD</label>
<button type="button" onClick={() => setShowForgot(true)}
style={{
...f, fontSize: 11, color: T.linkColor,
background: 'none', border: 'none', cursor: 'pointer', padding: 0, transition: 'opacity .15s'
}}
onMouseEnter={e => e.currentTarget.style.opacity = '.7'}
onMouseLeave={e => e.currentTarget.style.opacity = '1'}>
Forgot password?
</button>
</div>
<div style={{ position: 'relative' }}>
<input
type={showPw ? 'text' : 'password'} value={password}
onChange={e => 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}
/>
<button type="button" onClick={() => setShowPw(v => !v)} style={{
position: 'absolute', right: 12, top: '50%', transform: 'translateY(-50%)',
background: 'none', border: 'none', cursor: 'pointer', padding: 0,
color: T.inputIcon, display: 'flex', alignItems: 'center',
}}>
{showPw ? <EyeOff size={15} /> : <Eye size={15} />}
</button>
</div>
</div>
<label style={{ display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer', marginTop: -4 }}>
<input type="checkbox" style={{ accentColor: T.acc, width: 14, height: 14 }} />
<span style={{ fontSize: 12, color: T.t2 }}>Remember me for 30 days</span>
</label>
<button type="submit" disabled={loading} style={{
...syne, width: '100%', padding: '13px', borderRadius: 13, border: 'none',
fontSize: 14, fontWeight: 600, cursor: loading ? 'not-allowed' : 'pointer',
background: loading ? T.t4 : T.btnBg, color: T.btnText,
boxShadow: loading ? 'none' : T.btnShadow,
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8,
transition: 'all .2s', opacity: loading ? .7 : 1, marginTop: 4,
}}
onMouseEnter={e => { if (!loading) e.currentTarget.style.transform = 'translateY(-1px)'; }}
onMouseLeave={e => e.currentTarget.style.transform = 'none'}>
{loading
? <><span className="lp-spin" style={{
display: 'inline-block', width: 16, height: 16,
border: `2px solid rgba(255,255,255,.3)`, borderTopColor: '#fff', borderRadius: '50%'
}} /> Signing in…</>
: <>Sign In <ArrowRight size={15} /></>
}
</button>
</form>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, margin: '22px 0' }}>
<div style={{ flex: 1, height: 1, background: T.dividerBg }} />
<span style={{ ...mono, fontSize: 10, letterSpacing: 2, color: T.dividerText }}>OR</span>
<div style={{ flex: 1, height: 1, background: T.dividerBg }} />
</div>
<button type="button" onClick={handleGoogle} disabled={googleLoading || loading} style={{
...f, width: '100%', padding: '12px', borderRadius: 13, cursor: 'pointer',
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 10,
background: T.socialBg, border: `1px solid ${T.socialBorder}`, color: T.socialText,
fontSize: 13, fontWeight: 500, transition: 'all .15s',
opacity: googleLoading ? .75 : 1,
}}
onMouseEnter={e => {
if (!googleLoading && !loading) {
e.currentTarget.style.borderColor = T.acc;
e.currentTarget.style.transform = 'translateY(-1px)';
}
}}
onMouseLeave={e => { e.currentTarget.style.borderColor = T.socialBorder; e.currentTarget.style.transform = 'none'; }}>
<GoogleIcon /> {googleLoading ? 'Redirecting to Google...' : 'Continue with Google'}
</button>
<p style={{ marginTop: 24, textAlign: 'center', fontSize: 12, color: T.t2 }}>
Don't have an account?{' '}
<button type="button" onClick={() => setShowSignup(true)} style={{
...f, fontSize: 12, fontWeight: 600, color: T.linkColor,
background: 'none', border: 'none', cursor: 'pointer', padding: 0,
}}>
Create one free →
</button>
</p>
<p style={{
...mono, marginTop: 20, textAlign: 'center', fontSize: 10,
letterSpacing: .5, color: T.footerText
}}>
Secured by PolicyLens · v2.4.1 · SOC 2 Compliant
</p>
</div>
</div>
</div>
{showSignup && (
<SignupModal
dark={dark}
T={T}
onClose={() => setShowSignup(false)}
onSuccess={(user) => {
setShowSignup(false);
onSignupSuccess?.(user);
}}
/>
)}
{showForgot && (
<ForgotModal
dark={dark}
T={T}
onClose={() => setShowForgot(false)}
/>
)}
</div>
);
}