import React, { useCallback, useEffect, useRef, useState } from 'react'; /* ========================================================================== Session helpers (shared by the modal and the rest of the app) ========================================================================== */ export const AUTH_TOKEN_KEY = 'nexus_auth_token'; export const AUTH_USER_KEY = 'nexus_user'; const AUTH_API = '/api/v1/auth'; export function saveSession(token, user) { try { if (token) localStorage.setItem(AUTH_TOKEN_KEY, token); if (user) localStorage.setItem(AUTH_USER_KEY, JSON.stringify(user)); } catch (err) { console.warn('Unable to persist session:', err); } } export function clearSession() { try { localStorage.removeItem(AUTH_TOKEN_KEY); localStorage.removeItem(AUTH_USER_KEY); } catch { /* storage unavailable */ } } export function getStoredSession() { try { const token = localStorage.getItem(AUTH_TOKEN_KEY); const rawUser = localStorage.getItem(AUTH_USER_KEY); return { token, user: rawUser ? JSON.parse(rawUser) : null }; } catch { return { token: null, user: null }; } } // Tolerates empty / HTML error bodies (e.g. a gateway error page) instead of throwing async function readJson(response) { const text = await response.text(); try { return text ? JSON.parse(text) : {}; } catch { return {}; } } /** fetch() wrapper that attaches `Authorization: Bearer ` and parses JSON. */ export async function authFetch(url, options = {}) { const { token } = getStoredSession(); const headers = { Accept: 'application/json', ...(options.headers || {}) }; if (options.body && !headers['Content-Type']) headers['Content-Type'] = 'application/json'; if (token) headers.Authorization = `Bearer ${token}`; const response = await fetch(url, { ...options, headers }); const data = await readJson(response); if (response.status === 401 && token) clearSession(); return { ok: response.ok, status: response.status, data }; } /** Loads the signed-in user's profile from the API and refreshes `nexus_user`. */ export async function fetchCurrentUser() { const { ok, data } = await authFetch(`${AUTH_API}/me`); if (!ok || !data.user) return null; saveSession(null, data.user); return data.user; } /** * Call once on page load. Platform OAuth callbacks redirect back to the app with * the session in the URL fragment (#auth_token=…&auth_provider=…, or #auth_error=…). * This stores the session, strips the fragment from the address bar and reports the outcome. */ export async function consumeOAuthRedirect() { if (typeof window === 'undefined' || !window.location.hash) return null; const params = new URLSearchParams(window.location.hash.slice(1)); const token = params.get('auth_token'); const error = params.get('auth_error'); if (!token && !error) return null; const cleanUrl = window.location.pathname + window.location.search; window.history.replaceState(null, '', cleanUrl); const provider = params.get('auth_provider'); if (error) return { status: 'error', provider, error }; saveSession(token, null); const user = await fetchCurrentUser(); if (!user) { clearSession(); return { status: 'error', provider, error: 'Could not load your account. Please try again.' }; } return { status: params.get('auth_status') || 'success', provider, user, token }; } /* ========================================================================== Modal ========================================================================== */ const PLATFORMS = [ { id: 'xbox', label: 'Xbox', glyph: 'X', className: 'border-emerald-500/40 bg-emerald-500/10 text-emerald-300 hover:bg-emerald-500/20 focus-visible:ring-emerald-400', }, { id: 'playstation', label: 'PlayStation', glyph: 'PS', className: 'border-blue-500/40 bg-blue-500/10 text-blue-300 hover:bg-blue-500/20 focus-visible:ring-blue-400', }, { id: 'steam', label: 'Steam', glyph: 'S', className: 'border-slate-500/50 bg-slate-700/30 text-slate-200 hover:bg-slate-700/60 focus-visible:ring-slate-300', }, { id: 'nintendo', label: 'Nintendo', glyph: 'N', className: 'border-red-500/40 bg-red-500/10 text-red-300 hover:bg-red-500/20 focus-visible:ring-red-400', }, ]; const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; function validate(mode, email, password) { if (!EMAIL_PATTERN.test(email.trim())) return 'Please enter a valid email address.'; if (!password) return 'Please enter your password.'; if (mode === 'signup' && password.length < 8) return 'Password must be at least 8 characters long.'; if (password.length > 72) return 'Password cannot exceed 72 characters.'; return ''; } /** * Unified Sign Up / Log In modal. * * Props: * isOpen – show/hide the modal * onClose – called when the user dismisses the modal * onAuthSuccess – called with `{ token, user }` after a successful email login/signup * initialMode – 'signup' (default) or 'login' * returnTo – path to land on after a platform OAuth round-trip (defaults to the current page) */ export function AuthModal({ isOpen, onClose, onAuthSuccess, initialMode = 'signup', returnTo }) { const [mode, setMode] = useState(initialMode); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [showPassword, setShowPassword] = useState(false); const [loading, setLoading] = useState(false); const [pendingProvider, setPendingProvider] = useState(null); const [error, setError] = useState(''); const emailRef = useRef(null); const busy = loading || Boolean(pendingProvider); // Reset to a clean form whenever the modal is opened useEffect(() => { if (!isOpen) return; setMode(initialMode); setPassword(''); setError(''); setLoading(false); setPendingProvider(null); const id = setTimeout(() => emailRef.current?.focus(), 30); return () => clearTimeout(id); }, [isOpen, initialMode]); const handleClose = useCallback(() => { if (!busy && onClose) onClose(); }, [busy, onClose]); useEffect(() => { if (!isOpen) return; const onKeyDown = (e) => e.key === 'Escape' && handleClose(); window.addEventListener('keydown', onKeyDown); return () => window.removeEventListener('keydown', onKeyDown); }, [isOpen, handleClose]); if (!isOpen) return null; const switchMode = (next) => { if (busy || next === mode) return; setMode(next); setError(''); }; const handleEmailAuth = async (e) => { e.preventDefault(); if (busy) return; const validationError = validate(mode, email, password); if (validationError) { setError(validationError); return; } setLoading(true); setError(''); try { const response = await fetch(`${AUTH_API}/${mode === 'signup' ? 'signup' : 'login'}`, { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify({ email: email.trim(), password }), }); const data = await readJson(response); if (!response.ok || !data.token) { setError(data.error || `Request failed (${response.status}). Please try again.`); return; } saveSession(data.token, data.user); setPassword(''); if (onAuthSuccess) onAuthSuccess({ token: data.token, user: data.user, mode }); if (onClose) onClose(); } catch (err) { console.error('Auth request failed:', err); setError('Network error. Please check your connection and try again.'); } finally { setLoading(false); } }; const handleOAuth = async (provider) => { if (busy) return; setPendingProvider(provider); setError(''); const params = new URLSearchParams({ return_to: returnTo || window.location.pathname + window.location.search, }); try { // Already signed in? Link the platform to the current account instead of creating a new one. if (getStoredSession().token) { const { ok, data } = await authFetch(`${AUTH_API}/oauth/ticket`, { method: 'POST' }); if (ok && data.ticket) params.set('ticket', data.ticket); } window.location.assign(`${AUTH_API}/${provider}?${params.toString()}`); } catch (err) { console.error('OAuth start failed:', err); setError('Network error. Please check your connection and try again.'); setPendingProvider(null); } }; const isSignup = mode === 'signup'; return (
e.target === e.currentTarget && handleClose()} >

{isSignup ? 'Join the Nexus' : 'Welcome back'}

{isSignup ? 'Create your account to claim a gamertag and squad up.' : 'Log in to pick up where you left off.'}

{/* Tabs */}
{[ ['signup', 'Sign Up'], ['login', 'Log In'], ].map(([value, label]) => ( ))}
{error && (
{error}
)} {/* Email / password */}
setEmail(e.target.value)} placeholder="gamer@nexusgamers.online" className="w-full rounded-xl border border-slate-800 bg-slate-950 px-4 py-2.5 text-sm text-white placeholder-slate-600 transition focus:border-cyan-400 focus:outline-none disabled:opacity-60" />
setPassword(e.target.value)} placeholder={isSignup ? 'At least 8 characters' : '••••••••'} className="w-full rounded-xl border border-slate-800 bg-slate-950 py-2.5 pl-4 pr-16 text-sm text-white placeholder-slate-600 transition focus:border-cyan-400 focus:outline-none disabled:opacity-60" />
{/* Divider */}
{/* Platform OAuth */}
{PLATFORMS.map((platform) => ( ))}

{isSignup ? 'Already have an account? ' : 'New to Nexus? '}

); } export default AuthModal;