// CalGrillz — Login (email + password OR email + OTP code). // // Customers without a password fall back to the code path automatically. // Mode toggle lives at the top of the right-hand form column. function CGWLoginPage() { // mode: 'password' | 'code'. step: 'enter' | 'otp' | 'twofa' const [mode, setMode] = useState('password'); const [step, setStep] = useState('enter'); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [otp, setOtp] = useState(['','','','','','']); const [error, setError] = useState(''); const [info, setInfo] = useState(''); const [busy, setBusy] = useState(false); const [resendIn, setResendIn] = useState(0); const refs = useRef([]); // 2FA challenge state (set when the server asks for a second factor). const [twoFa, setTwoFa] = useState(null); // { method, label, emailHint } const [twoFaCode, setTwoFaCode] = useState(''); const URLS = window.CG_URLS || {}; const CSRF = URLS.csrf || ''; // Optional return-to path after login (e.g. arriving from the public live // draw as ?next=/draw/38). Same-origin ROOT-relative paths only — never an // absolute or //host URL — so it can't be used as an open redirect. const safeNext = () => { try { const n = new URLSearchParams(window.location.search).get('next'); // Must be a plain root-relative path: starts '/', not '//' or '/\', and no // backslash / control chars (browsers normalise those into '//host'). Then // confirm it still resolves to THIS origin. Blocks open-redirect payloads // like /%5Cevil.com or /%09/evil.com. if (!n || n.charAt(0) !== '/' || n.charAt(1) === '/' || /[\\\t\n\r\x00]/.test(n)) return null; const u = new URL(n, window.location.origin); return u.origin === window.location.origin ? (u.pathname + u.search + u.hash) : null; } catch (e) {} return null; }; useEffect(() => { if (resendIn > 0) { const t = setInterval(() => setResendIn(s => Math.max(0, s - 1)), 1000); return () => clearInterval(t); } }, [resendIn]); const emailValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim()); async function signInWithPassword() { setError(''); if (! emailValid) { setError('Enter your email.'); return; } if (! password) { setError('Enter your password.'); return; } setBusy(true); try { const res = await fetch(URLS.loginPassword || '/login/password', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', 'X-CSRF-TOKEN': CSRF }, body: JSON.stringify({ email: email.trim(), password }), }); const data = await res.json().catch(() => ({})); if (! res.ok || ! data.ok) { setError(data.error || 'Email and password do not match.'); return; } if (data.two_factor_required) { enterTwoFa(data); return; } window.location.href = safeNext() || data.redirect_to || (URLS.rewards || '/'); } catch (e) { setError('Network hiccup. Try once more.'); } finally { setBusy(false); } } // A second factor is required — switch to the 2FA challenge step. function enterTwoFa(data) { setTwoFa({ method: data.method, label: data.method_label || 'code', emailHint: data.email_hint || null }); setTwoFaCode(''); setError(''); setInfo(''); setStep('twofa'); if (data.method === 'email') setResendIn(30); } async function submitTwoFa() { const code = (twoFaCode || '').trim(); if (! code) { setError('Enter your code.'); return; } setError(''); setBusy(true); try { const res = await fetch(URLS.login2fa || '/login/2fa', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', 'X-CSRF-TOKEN': CSRF }, body: JSON.stringify({ code }), }); const data = await res.json().catch(() => ({})); if (! res.ok || ! data.ok) { setError(data.error || 'That code is not correct.'); if (data.restart) { setStep('enter'); setTwoFa(null); } return; } window.location.href = safeNext() || data.redirect_to || (URLS.rewards || '/'); } catch (e) { setError('Network hiccup. Try once more.'); } finally { setBusy(false); } } async function resendTwoFa() { setError(''); setInfo(''); try { const res = await fetch(URLS.login2faResend || '/login/2fa/resend', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', 'X-CSRF-TOKEN': CSRF }, body: JSON.stringify({}), }); const data = await res.json().catch(() => ({})); if (data.restart) { setStep('enter'); setTwoFa(null); setError(data.error || 'Please sign in again.'); return; } setResendIn(30); setInfo('New code sent to your email.'); } catch (e) { setError('Could not resend. Try again.'); } } async function sendCode() { setError(''); setInfo(''); if (! emailValid) { setError('Enter a valid email.'); return; } setBusy(true); try { const res = await fetch(URLS.loginRequest || '/login/request', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', 'X-CSRF-TOKEN': CSRF }, body: JSON.stringify({ email: email.trim() }), }); const data = await res.json().catch(() => ({})); // New / unregistered email → registration is required (name + phone). // Hand off to the signup page, carrying the typed email for prefill. if (data.needs_registration && data.redirect_to) { try { sessionStorage.setItem('cg_signup_email', email.trim()); } catch (e) {} window.location.href = data.redirect_to; return; } if (! res.ok || ! data.ok) { setError(data.error || 'Could not send the code.'); return; } setMode('code'); setStep('otp'); setResendIn(30); if (data.dev_code) setInfo('Dev mode code: ' + data.dev_code); else setInfo('Code sent. Check your inbox (and spam folder).'); setTimeout(() => refs.current[0]?.focus(), 50); } catch (e) { setError('Network hiccup. Try once more.'); } finally { setBusy(false); } } const setOtpChar = (i, ch) => { const v = ch.replace(/\D/g, '').slice(-1); const next = [...otp]; next[i] = v; setOtp(next); if (v && i < 5) refs.current[i + 1]?.focus(); }; const onOtpKey = (i, e) => { if (e.key === 'Backspace' && !otp[i] && i > 0) refs.current[i - 1]?.focus(); }; const onOtpPaste = (e) => { const v = (e.clipboardData.getData('text') || '').replace(/\D/g, '').slice(0, 6).split(''); if (v.length) { const next = ['','','','','','']; v.forEach((c, i) => next[i] = c); setOtp(next); refs.current[Math.min(v.length, 5)]?.focus(); e.preventDefault(); } }; async function verify() { const code = otp.join(''); if (code.length !== 6) { setError('Enter all 6 digits.'); return; } setError(''); setBusy(true); try { const res = await fetch(URLS.loginVerify || '/login/verify', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', 'X-CSRF-TOKEN': CSRF }, body: JSON.stringify({ email: email.trim(), code }), }); const data = await res.json().catch(() => ({})); if (data.needs_registration && data.redirect_to) { try { sessionStorage.setItem('cg_signup_email', email.trim()); } catch (e) {} window.location.href = data.redirect_to; return; } if (! res.ok || ! data.ok) { setError(data.error || 'Wrong code or expired.'); return; } if (data.two_factor_required) { enterTwoFa(data); return; } window.location.href = safeNext() || data.redirect_to || (URLS.rewards || '/'); } catch (e) { setError('Network hiccup. Try once more.'); } finally { setBusy(false); } } // ─── Mode toggle (segmented control) ──────────────────────────────────── const Toggle = (
{['password', 'code'].map(m => ( ))}
); return (
{/* LEFT — image */}
{Array.from({ length: 20 }).map((_, i) => ( ))}
Pick up where you left off

Your rewards,
streak, and
vouchers.

Sign in with your password, or skip it and we'll email you a 6-digit code.

{/* RIGHT — form */}
{step === 'enter' && ( <>
Sign in

Welcome to
the pit.

Use your password or get a one-time code.

{Toggle} {/* Real
with proper name/autoComplete pairs — without a form container, browsers can't reliably resolve credentials and sometimes offer phone-number suggestions instead of emails. */} { e.preventDefault(); mode === 'password' ? signInWithPassword() : sendCode(); }} >
✉️ { setEmail(e.target.value); setError(''); }} placeholder="you@example.com" className="cg-input" style={{ flex: 1, background: 'transparent', border: 'none', outline: 'none', color: 'var(--cream)', fontFamily: 'var(--font-mono)', fontSize: 16, padding: '16px 0', letterSpacing: '0.04em', }} />
{mode === 'password' && (
{ setPassword(e.target.value); setError(''); }} placeholder="Your password" className="cg-input" style={{ width: '100%', marginTop: 8, padding: '16px 18px', background: 'var(--charcoal-800)', border: '1px solid var(--charcoal-700)', borderRadius: 14, color: 'var(--cream)', outline: 'none', fontSize: 15, fontFamily: 'var(--font-mono)', }} />
)} {error &&
{error}
}
Don't have an account?{' '} Sign up
By continuing you agree to our
Terms & Privacy policy
)} {step === 'otp' && ( <>
Step 2 of 2

Type the
code we sent.

We emailed {email} a 6-digit code.

{info && (
{info}
)}
{otp.map((d, i) => ( refs.current[i] = el} type="text" inputMode="numeric" maxLength={1} value={d} onChange={(e) => setOtpChar(i, e.target.value)} onKeyDown={(e) => onOtpKey(i, e)} onPaste={onOtpPaste} style={{ flex: '1 1 0', minWidth: 0, maxWidth: 56, height: 62, padding: 0, background: 'var(--charcoal-800)', border: '1.5px solid ' + (d ? 'var(--ember-500)' : 'var(--charcoal-700)'), borderRadius: 12, textAlign: 'center', fontFamily: 'var(--font-display)', fontSize: 26, color: 'var(--cream)', outline: 'none', boxSizing: 'border-box', transition: 'border-color 0.15s', }} /> ))}
{error &&
{error}
}
Didn't get it? {resendIn > 0 ? Resend in {resendIn}s : }
)} {step === 'twofa' && twoFa && ( <>
Two-factor · security check

One more
step.

{twoFa.method === 'pin' && <>Enter your account PIN to finish signing in.} {twoFa.method === 'totp' && <>Open your authenticator app and enter the 6-digit code.} {twoFa.method === 'email' && <>We sent a 6-digit code to {twoFa.emailHint || 'your email'}.}

{info && (
{info}
)}
{ e.preventDefault(); submitTwoFa(); }} style={{ marginTop: 24 }}> { setTwoFaCode(e.target.value.replace(/\D/g, '')); setError(''); }} placeholder={twoFa.method === 'pin' ? 'Your PIN' : '6-digit code'} style={{ width: '100%', padding: '18px', textAlign: 'center', letterSpacing: '0.5em', background: 'var(--charcoal-800)', border: '1.5px solid ' + (error ? 'var(--danger)' : 'var(--charcoal-700)'), borderRadius: 14, color: 'var(--cream)', outline: 'none', fontFamily: 'var(--font-mono)', fontSize: 24, }} /> {error &&
{error}
}
{twoFa.method === 'email' && (resendIn > 0 ? Resend in {resendIn}s : )}
)}
); } ReactDOM.createRoot(document.getElementById('root')).render();