import { useEffect, useMemo, useRef, useState } from 'react' import L from 'leaflet' import 'leaflet/dist/leaflet.css' import Tree from '../Tree/Tree' import FallingLeaves from '../Tree/FallingLeaves' const numberFormat = new Intl.NumberFormat('en-GB') const dateFormat = new Intl.DateTimeFormat('en-GB', { dateStyle: 'medium', timeStyle: 'short', }) const apiEndpoints = { health: '/health', uptime: '/api/uptime', viewers: '/api/viewers', viewerEvent: '/api/viewers/event', viewerDelete: '/api/viewers/delete', teams: '/api/tss/leaderboard/teams?limit=100', teamsHealth: '/api/tss/leaderboard/teams?limit=1', resolve: (name) => `/api/tss/teams/resolve?name=${encodeURIComponent(name)}`, detail: (name) => `/api/tss/teams/${encodeURIComponent(name)}`, history: (name) => `/api/tss/teams/${encodeURIComponent(name)}/history`, games: (name) => `/api/tss/teams/${encodeURIComponent(name)}/games`, } const navItems = [ { path: '/', label: 'Home' }, { path: '/teams', label: 'Team leaderboard' }, { path: '/battle-logs', label: 'Battle Logs' }, { path: '/viewers', label: 'Viewers' }, ] const analyticsConsentKey = 'tssbot.analyticsConsent' const analyticsPreferencesKey = 'tssbot.analyticsPreferences' const analyticsPreferencesCookie = 'tssbot_analytics_preferences' const analyticsVisitorKey = 'tssbot.analyticsVisitor' const analyticsConsentVersion = 3 const turnstileSiteKey = import.meta.env.VITE_TURNSTILE_SITE_KEY || '' const defaultAnalyticsPreferences = { chosen: false, analytics: false, device: false, display: false, locale: false, referrer: false, diagnostics: false, version: analyticsConsentVersion, } async function fetchJson(path, signal) { const response = await fetch(path, { signal, headers: { Accept: 'application/json' }, }) const body = await response.json().catch(() => null) if (!response.ok) { throw new Error(body?.error || `Request failed with ${response.status}`) } return body } function parseRoute(pathname = window.location.pathname) { if (pathname === '/') return { page: 'home', teamName: '' } if (pathname === '/teams') return { page: 'teams', teamName: '' } if (pathname === '/uptime') return { page: 'uptime', teamName: '' } if (pathname === '/viewers') return { page: 'viewers', teamName: '' } if (pathname === '/privacy') return { page: 'privacy', teamName: '' } if (pathname.startsWith('/teams/')) { const teamName = decodeURIComponent(pathname.slice('/teams/'.length)) return { page: 'team', teamName } } if (pathname === '/battle-logs' || pathname === '/live') return { page: 'battle-logs', teamName: '' } return { page: 'home', teamName: '' } } function teamPath(name) { return `/teams/${encodeURIComponent(name)}` } function formatNumber(value) { return numberFormat.format(Number(value || 0)) } function formatDate(timestamp) { if (!timestamp) return 'Unknown time' return dateFormat.format(new Date(Number(timestamp) * 1000)) } function bestTeamName(team) { return team?.tag_name || team?.short_name || team?.long_name || '' } function normalizeAnalyticsPreferences(value) { if (!value || typeof value !== 'object') return { ...defaultAnalyticsPreferences } return { chosen: Boolean(value.chosen) && value.version === analyticsConsentVersion, analytics: Boolean(value.analytics), device: Boolean(value.device), display: Boolean(value.display), locale: Boolean(value.locale), referrer: Boolean(value.referrer), diagnostics: Boolean(value.diagnostics), version: analyticsConsentVersion, } } function browserPrivacySignalEnabled() { return Boolean( navigator.globalPrivacyControl || navigator.doNotTrack === '1' || window.doNotTrack === '1', ) } function browserConnectionDetails() { const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection || null if (!connection) return {} return { effective_type: connection.effectiveType || '', downlink_mbps: Number.isFinite(connection.downlink) ? connection.downlink : null, rtt_ms: Number.isFinite(connection.rtt) ? connection.rtt : null, save_data: Boolean(connection.saveData), } } function browserDiagnosticsMetadata() { return { cookies_enabled: navigator.cookieEnabled, do_not_track: navigator.doNotTrack || window.doNotTrack || '', global_privacy_control: Boolean(navigator.globalPrivacyControl), online: navigator.onLine, platform: navigator.platform || '', vendor: navigator.vendor || '', max_touch_points: navigator.maxTouchPoints || 0, hardware_concurrency: navigator.hardwareConcurrency || null, device_memory_gb: navigator.deviceMemory || null, webdriver: Boolean(navigator.webdriver), history_length: window.history.length, visibility_state: document.visibilityState, connection: browserConnectionDetails(), } } function readCookie(name) { try { const match = document.cookie .split('; ') .find((item) => item.startsWith(`${encodeURIComponent(name)}=`)) return match ? decodeURIComponent(match.split('=').slice(1).join('=')) : '' } catch { return '' } } function writeCookie(name, value) { try { const sixMonths = 60 * 60 * 24 * 180 document.cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}; Max-Age=${sixMonths}; Path=/; SameSite=Lax` } catch { // Some browsers block cookies; local storage and in-memory state still work. } } function storedAnalyticsPreferences() { const cookieValue = readCookie(analyticsPreferencesCookie) if (cookieValue) { try { return normalizeAnalyticsPreferences(JSON.parse(cookieValue)) } catch { // Fall back through the older storage formats below. } } try { const stored = window.localStorage.getItem(analyticsPreferencesKey) if (stored) return normalizeAnalyticsPreferences(JSON.parse(stored)) } catch { // Fall back through the older consent flag below. } try { const legacyConsent = window.localStorage.getItem(analyticsConsentKey) || '' if (legacyConsent === 'analytics') { return { ...defaultAnalyticsPreferences, chosen: true, analytics: true, device: true, display: true, locale: true, referrer: true, diagnostics: true, } } if (legacyConsent === 'declined') { return { ...defaultAnalyticsPreferences, chosen: true, analytics: false } } } catch { // Use the default prompt state. } return { ...defaultAnalyticsPreferences } } function persistAnalyticsPreferences(preferences) { const normalized = normalizeAnalyticsPreferences(preferences) const serialized = JSON.stringify(normalized) writeCookie(analyticsPreferencesCookie, serialized) try { window.localStorage.setItem(analyticsPreferencesKey, serialized) window.localStorage.setItem( analyticsConsentKey, normalized.analytics ? 'analytics' : 'declined', ) } catch { // Local storage can be blocked; the cookie and in-memory choice still apply. } return normalized } function stableId(storageKey) { try { const existing = window.localStorage.getItem(storageKey) if (existing) return existing const id = window.crypto?.randomUUID?.() || `${Date.now().toString(36)}${Math.random().toString(36).slice(2)}` window.localStorage.setItem(storageKey, id) return id } catch { return `${Date.now().toString(36)}${Math.random().toString(36).slice(2)}` } } function storedVisitorId(storageKey) { try { return window.localStorage.getItem(storageKey) || '' } catch { return '' } } function forgetVisitorId(storageKey) { try { window.localStorage.removeItem(storageKey) } catch { // Storage may be unavailable; nothing else to clear locally. } } const analyticsSessionId = window.crypto?.randomUUID?.() || `${Date.now().toString(36)}${Math.random().toString(36).slice(2)}` function browserName() { const ua = navigator.userAgent if (ua.includes('Edg/')) return 'Microsoft Edge' if (ua.includes('OPR/')) return 'Opera' if (ua.includes('Firefox/')) return 'Firefox' if (ua.includes('Chrome/') && !ua.includes('Chromium/')) return 'Chrome' if (ua.includes('Safari/') && ua.includes('Version/')) return 'Safari' return 'Unknown' } function operatingSystem() { const ua = navigator.userAgent if (ua.includes('Windows NT')) return 'Windows' if (ua.includes('Android')) return 'Android' if (/iPhone|iPad|iPod/.test(ua)) return 'iOS' if (ua.includes('Mac OS X')) return 'macOS' if (ua.includes('Linux')) return 'Linux' return 'Unknown' } function deviceType() { const ua = navigator.userAgent if (/Mobi|Android|iPhone|iPod/.test(ua)) return 'Mobile' if (/iPad|Tablet/.test(ua)) return 'Tablet' return 'Desktop' } function routeLabel(route) { if (route.page === 'team' && route.teamName) return `Team: ${route.teamName}` if (route.page === 'teams') return 'Team leaderboard' if (route.page === 'battle-logs') return 'Battle Logs' if (route.page === 'uptime') return 'Uptime' if (route.page === 'viewers') return 'viewers' if (route.page === 'privacy') return 'Privacy notice' return 'Home' } async function fetchRecentTssGames(teams, signal) { const teamNames = teams.map(bestTeamName).filter(Boolean).slice(0, 12) if (!teamNames.length) { return { matches: [] } } const responses = await Promise.allSettled( teamNames.map((name) => fetchJson(apiEndpoints.games(name), signal).then((data) => ({ name, data }))), ) const bySession = new Map() responses.forEach((result) => { if (result.status !== 'fulfilled') return const { name, data } = result.value ;(data.games || []).forEach((game) => { if (!game.session_id) return const existing = bySession.get(game.session_id) const currentTimestamp = Number(game.timestamp || 0) if (existing && Number(existing.timestamp || 0) >= currentTimestamp) return bySession.set(game.session_id, { ...game, team_name: data.tag_name || name, long_name: data.long_name || '', }) }) }) return { matches: Array.from(bySession.values()) .sort((a, b) => Number(b.timestamp || 0) - Number(a.timestamp || 0)) .slice(0, 50), } } function Stat({ label, value }) { return (

{label}

{value}

) } function TurnstileWidget({ siteKey, theme = 'auto', size = 'normal', action, onVerify, onExpire, onError, resetSignal = 0 }) { const containerRef = useRef(null) const widgetIdRef = useRef(null) const callbacksRef = useRef({ onVerify, onExpire, onError }) useEffect(() => { callbacksRef.current = { onVerify, onExpire, onError } }, [onVerify, onExpire, onError]) useEffect(() => { if (!siteKey) return undefined const container = containerRef.current if (!container) return undefined let cancelled = false let pollTimer const renderWidget = () => { if (cancelled) return if (!window.turnstile || typeof window.turnstile.render !== 'function') { pollTimer = window.setTimeout(renderWidget, 150) return } const options = { sitekey: siteKey, theme, size, callback: (token) => callbacksRef.current.onVerify?.(token), 'expired-callback': () => callbacksRef.current.onExpire?.(), 'error-callback': () => callbacksRef.current.onError?.(), } if (action) options.action = action widgetIdRef.current = window.turnstile.render(container, options) } renderWidget() return () => { cancelled = true window.clearTimeout(pollTimer) const widgetId = widgetIdRef.current widgetIdRef.current = null if (widgetId != null && window.turnstile?.remove) { try { window.turnstile.remove(widgetId) } catch { /* widget already gone */ } } } }, [siteKey, theme, size, action]) useEffect(() => { if (!resetSignal) return const widgetId = widgetIdRef.current if (widgetId != null && window.turnstile?.reset) { try { window.turnstile.reset(widgetId) } catch { /* widget gone or already reset */ } } }, [resetSignal]) if (!siteKey) return null return
} function SiteGate({ onVerified }) { const [status, setStatus] = useState('idle') const [error, setError] = useState('') const [resetSignal, setResetSignal] = useState(0) const submittingRef = useRef(false) useEffect(() => { const previousOverflow = document.body.style.overflow document.body.style.overflow = 'hidden' return () => { document.body.style.overflow = previousOverflow } }, []) async function handleVerify(token) { if (submittingRef.current) return submittingRef.current = true setStatus('verifying') setError('') try { const response = await fetch('/api/turnstile/verify', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ token }), }) if (!response.ok) { setError('Verification failed — please try again.') setStatus('idle') setResetSignal((value) => value + 1) return } setStatus('verified') onVerified() } catch { setError('Network error — please retry.') setStatus('idle') setResetSignal((value) => value + 1) } finally { submittingRef.current = false } } return (

Verifying you are human

This quick check protects the site from automated abuse. It usually clears itself.

setError('Challenge expired — please solve it again.')} onError={() => setError('Challenge could not load. Refresh to retry.')} resetSignal={resetSignal} /> {status === 'verifying' ? (

Confirming with Cloudflare…

) : null} {error ?

{error}

: null}
) } function App() { const [gateState, setGateState] = useState(turnstileSiteKey ? 'checking' : 'verified') useEffect(() => { if (!turnstileSiteKey) return undefined let cancelled = false fetch('/api/turnstile/session', { headers: { Accept: 'application/json' } }) .then((response) => (response.ok ? response.json() : { verified: false })) .then((data) => { if (cancelled) return setGateState(data?.verified ? 'verified' : 'required') }) .catch(() => { if (!cancelled) setGateState('required') }) return () => { cancelled = true } }, []) if (gateState === 'checking') { return