// ============== auth context + modals ==============
const AuthContext = React.createContext({ user: null, loading: true, login: null, register: null, logout: null });
function AuthProvider({ children }) {
const [user, setUser] = React.useState(null);
const [loading, setLoading] = React.useState(true);
const fetchMe = React.useCallback(async () => {
if (!window.W3J_API.tokens.access) { setUser(null); setLoading(false); return; }
try {
const me = await window.W3J_API.apiFetch("/auth/me");
setUser(me);
} catch {
window.W3J_API.tokens.clear();
setUser(null);
} finally {
setLoading(false);
}
}, []);
React.useEffect(() => {
fetchMe();
const onChange = () => fetchMe();
window.addEventListener("w3j:auth-changed", onChange);
return () => window.removeEventListener("w3j:auth-changed", onChange);
}, [fetchMe]);
const login = React.useCallback(async (email, password) => {
const tokens = await window.W3J_API.apiFetch("/auth/login", { method: "POST", body: { email, password }, auth: false });
window.W3J_API.tokens.set(tokens);
}, []);
const register = React.useCallback(async (email, password, full_name) => {
const tokens = await window.W3J_API.apiFetch("/auth/register", { method: "POST", body: { email, password, full_name }, auth: false });
window.W3J_API.tokens.set(tokens);
}, []);
const logout = React.useCallback(() => { window.W3J_API.tokens.clear(); }, []);
const loginWithGoogle = React.useCallback(async (credential) => {
const tokens = await window.W3J_API.apiFetch("/auth/google", { method: "POST", body: { credential }, auth: false });
window.W3J_API.tokens.set(tokens);
}, []);
return (
{children}
);
}
// --- Google Sign-In button ---
let _gisLoaded = null;
function loadGoogleScript() {
if (_gisLoaded) return _gisLoaded;
_gisLoaded = new Promise((resolve, reject) => {
if (window.google?.accounts?.id) return resolve();
const s = document.createElement("script");
s.src = "https://accounts.google.com/gsi/client";
s.async = true; s.defer = true;
s.onload = () => resolve();
s.onerror = () => reject(new Error("failed to load Google Sign-In"));
document.head.appendChild(s);
});
return _gisLoaded;
}
function GoogleButton({ onSuccess }) {
const ref = React.useRef(null);
const { loginWithGoogle } = useAuth();
const [clientId, setClientId] = React.useState(null);
const [err, setErr] = React.useState("");
React.useEffect(() => {
window.W3J_API.apiFetch("/auth/google/config", { auth: false })
.then((cfg) => { if (cfg.enabled) setClientId(cfg.client_id); })
.catch(() => {});
}, []);
React.useEffect(() => {
if (!clientId || !ref.current) return;
let cancelled = false;
loadGoogleScript().then(() => {
if (cancelled) return;
window.google.accounts.id.initialize({
client_id: clientId,
callback: async (response) => {
try {
await loginWithGoogle(response.credential);
onSuccess && onSuccess();
} catch (ex) { setErr(ex.message || "Google sign-in failed"); }
},
});
window.google.accounts.id.renderButton(ref.current, {
theme: "filled_black", size: "large", type: "standard",
shape: "pill", text: "continue_with", width: 320,
});
}).catch((ex) => setErr(ex.message));
return () => { cancelled = true; };
}, [clientId, loginWithGoogle, onSuccess]);
if (!clientId) return null;
return (
);
}
window.GoogleButton = GoogleButton;
const useAuth = () => React.useContext(AuthContext);
function AuthModal({ mode: initialMode = "login", onClose, onSuccess }) {
const [mode, setMode] = React.useState(initialMode);
const [email, setEmail] = React.useState("");
const [password, setPassword] = React.useState("");
const [fullName, setFullName] = React.useState("");
const [err, setErr] = React.useState("");
const [busy, setBusy] = React.useState(false);
const { login, register } = useAuth();
React.useEffect(() => {
const onKey = (e) => { if (e.key === "Escape") onClose(); };
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, [onClose]);
const submit = async (e) => {
e.preventDefault();
setErr(""); setBusy(true);
try {
if (mode === "login") await login(email, password);
else await register(email, password, fullName);
onSuccess && onSuccess();
onClose();
} catch (ex) {
setErr(ex.message || "Something went wrong");
} finally {
setBusy(false);
}
};
return (
e.stopPropagation()} role="dialog" aria-modal="true">
{mode === "login" ? "Sign in" : "Create account"}
{mode === "login" ? "Welcome back." : <>Build a candidate file.>}
);
}
window.AuthProvider = AuthProvider;
window.useAuth = useAuth;
window.AuthModal = AuthModal;