// auth.jsx — the login gate. AuthGate decides which screen to show: // • checking → a quiet "Loading…" // • signed out → (log in / register) // • signed in, unverified → (check your email) // • signed in, verified → (the dashboard, from app.jsx) // Loaded *after* app.jsx so and are already in scope, and // it owns the single ReactDOM root. function useAuth() { const [state, setState] = React.useState({ status: "checking", user: null }); const refresh = React.useCallback(() => { setState((s) => ({ ...s, status: s.status === "checking" ? "checking" : s.status })); fetch("/auth/me", { credentials: "same-origin" }) .then((r) => (r.ok ? r.json() : null)) .then((user) => setState({ status: user ? "in" : "out", user: user || null })) .catch(() => setState({ status: "out", user: null })); }, []); React.useEffect(refresh, [refresh]); return { ...state, refresh, setOut: () => setState({ status: "out", user: null }), }; } function LoginView({ onAuthed }) { const [tab, setTab] = React.useState("login"); // "login" | "register" const [identifier, setIdentifier] = React.useState(""); const [username, setUsername] = React.useState(""); const [email, setEmail] = React.useState(""); const [password, setPassword] = React.useState(""); const [error, setError] = React.useState(null); const [busy, setBusy] = React.useState(false); const submit = (e) => { e.preventDefault(); setError(null); setBusy(true); const isReg = tab === "register"; const body = isReg ? { username, email, password } : { identifier, password }; fetch(isReg ? "/auth/register" : "/auth/login", { method: "POST", credentials: "same-origin", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }) .then(async (r) => { if (!r.ok) { const d = await r.json().catch(() => ({})); const detail = d && d.detail; throw new Error( typeof detail === "string" ? detail : (detail && detail.message) || "Something went wrong" ); } onAuthed(); // re-checks /auth/me — AuthGate routes from there }) .catch((err) => setError(err.message)) .finally(() => setBusy(false)); }; return (

Every ticker,
one current price.

{tab === "register" ? ( <> setUsername(e.target.value)} /> setEmail(e.target.value)} /> ) : ( setIdentifier(e.target.value)} /> )} setPassword(e.target.value)} /> {error &&
{error}
}
{tab === "register" && (

We’ll email you a link to confirm your address.

)}
); } function VerifyNotice({ user, onRefresh, onLogout }) { const [busy, setBusy] = React.useState(false); const [note, setNote] = React.useState(null); const resend = () => { setBusy(true); setNote(null); fetch("/auth/resend-verification", { method: "POST", credentials: "same-origin", }) .then((r) => { if (!r.ok) throw new Error(); setNote("Sent — check your inbox."); }) .catch(() => setNote("Couldn't resend just now. Try again in a moment.")) .finally(() => setBusy(false)); }; return (

Confirm your email

We sent a confirmation link to {user.email}. Click it to activate your dashboard, then come back here.

{note &&
{note}
}
); } function AuthGate() { const auth = useAuth(); if (auth.status === "checking") { return
Loading…
; } if (auth.status === "out") { return ; } // status === "in" if (!auth.user.email_verified) { return ( { fetch("/auth/logout", { method: "POST", credentials: "same-origin" }) .finally(auth.setOut); }} /> ); } return ; } window.AuthGate = AuthGate; const root = ReactDOM.createRoot(document.getElementById("root")); root.render();