import { useEffect, useMemo, useRef, useState } from 'react' import Tree, { prewarmTreeCanvas } 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', homeTeams: '/api/tss/leaderboard/teams?limit=4', teamsHealth: '/api/tss/leaderboard/teams?limit=1', recentGames: '/api/tss/games/recent?limit=50', game: (gameId) => `/api/tss/games/${encodeURIComponent(gameId)}`, gameLogs: (gameId) => `/api/tss/games/${encodeURIComponent(gameId)}/logs`, resolve: (name) => `/api/tss/teams/resolve?name=${encodeURIComponent(name)}`, searchTeams: (name) => `/api/tss/teams/search?q=${encodeURIComponent(name)}&limit=10`, detail: (name) => `/api/tss/teams/${encodeURIComponent(name)}`, games: (name) => `/api/tss/teams/${encodeURIComponent(name)}/games`, player: (uid) => `/api/tss/player/${encodeURIComponent(uid)}`, } const navItems = [ { path: '/', label: 'Home' }, { path: '/teams', label: 'Team Leaderboard' }, { path: '/battle-logs', label: 'Battle Logs' }, { path: '/viewers', label: 'Viewers' }, { path: '/docs', label: 'Setup' }, ] const analyticsConsentKey = 'tssbot.analyticsConsent' const analyticsPreferencesKey = 'tssbot.analyticsPreferences' const analyticsPreferencesCookie = 'tssbot_analytics_preferences' const analyticsVisitorKey = 'tssbot.analyticsVisitor' const analyticsConsentVersion = 3 const themePreferenceKey = 'tssbot.theme' const themePreferenceCookie = 'tssbot_theme' const liveRefreshMs = 15000 const siteVersion = 'v1' 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 === '/docs') return { page: 'docs', teamName: '' } if (pathname.startsWith('/players/')) { const uid = decodeURIComponent(pathname.slice('/players/'.length)) return { page: 'player', teamName: '', uid } } if (pathname.startsWith('/games/')) { const gameId = decodeURIComponent(pathname.slice('/games/'.length)) return { page: 'game', teamName: '', gameId } } 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 gamePath(gameId) { return `/games/${encodeURIComponent(gameId)}` } function formatNumber(value) { return numberFormat.format(Number(value || 0)) } function formatMatchSize(playerCount) { const count = formatNumber(playerCount) return `${count}v${count}` } function formatDate(timestamp) { if (!timestamp) return 'Unknown time' return dateFormat.format(new Date(Number(timestamp) * 1000)) } function formatDuration(seconds) { const total = Math.round(Number(seconds || 0)) if (!total) return '' const m = Math.floor(total / 60) const s = total % 60 if (!m) return `${s}s` return `${m}m ${s}s` } function gameParticipants(game) { const winner = displayTeamName(game?.winning_team) const loser = displayTeamName(game?.losing_team) if (winner || loser) { return [ winner ? { name: winner, result: 'win' } : null, loser ? { name: loser, result: 'loss' } : null, ].filter(Boolean) } const fallbackName = game?.team_name || '' if (!fallbackName) return [] return [ { name: fallbackName, result: String(game?.result || '').toLowerCase() === 'win' ? 'win' : 'loss', }, ] } function displayTeamName(value) { return String(value || '').trim() } function ParticipantNames({ participants }) { if (!participants.length) { return

Participants unknown

} return (
{participants.map((participant) => ( {participant.name} ))}
) } function bestTeamName(team) { return team?.name || '' } function teamDetailLooksReal(detail) { if (!detail || typeof detail !== 'object') return false const summary = detail.team_summary || detail.squadron_summary || null const players = Array.isArray(detail.players) ? detail.players : [] const hasStableId = Boolean(detail.clan_id || detail.id || detail.team_id || detail.squadron_id) const hasRoster = players.length > 0 const hasActivity = Boolean( Number(summary?.player_count || 0) > 0 || Number(summary?.total_battles || 0) > 0 || Number(summary?.wins || 0) > 0 || Number(summary?.points?.total_points || summary?.total_points || 0) > 0, ) return hasStableId || hasRoster || hasActivity } function canonicalTeamName(detail, fallback = '') { return detail?.name || fallback } 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 normalizeThemePreference(value) { return value === 'dark' ? 'dark' : 'light' } function storedThemePreference() { const bootTheme = window.__TSS_BOOT_PREFERENCES__?.theme if (bootTheme) return normalizeThemePreference(bootTheme) const cookieValue = readCookie(themePreferenceCookie) if (cookieValue) return normalizeThemePreference(cookieValue) try { return normalizeThemePreference(window.localStorage.getItem(themePreferenceKey)) } catch { return 'light' } } function persistThemePreference(theme) { const normalized = normalizeThemePreference(theme) writeCookie(themePreferenceCookie, normalized) try { window.localStorage.setItem(themePreferenceKey, normalized) } catch { // Cookies still remember the theme when local storage is blocked. } return normalized } function storedAnalyticsPreferences() { const bootPreferences = window.__TSS_BOOT_PREFERENCES__?.analyticsPreferences if (bootPreferences) return normalizeAnalyticsPreferences(bootPreferences) 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 === 'game') return route.gameId ? `Game ${route.gameId}` : 'Game' if (route.page === 'uptime') return 'Uptime' if (route.page === 'viewers') return 'viewers' if (route.page === 'privacy') return 'Privacy notice' if (route.page === 'docs') return 'Docs' if (route.page === 'player') return route.uid ? `Player ${route.uid}` : 'Player' return 'Home' } function currentPublicOrigin() { return window.location.origin.replace(/\/$/, '') } function canonicalPathForRoute(route) { if (route.page === 'team' && route.teamName) return teamPath(route.teamName) if (route.page === 'teams') return '/teams' if (route.page === 'battle-logs') return '/battle-logs' if (route.page === 'game' && route.gameId) return gamePath(route.gameId) if (route.page === 'uptime') return '/uptime' if (route.page === 'viewers') return '/viewers' if (route.page === 'privacy') return '/privacy' if (route.page === 'docs') return '/docs' if (route.page === 'player' && route.uid) return `/players/${encodeURIComponent(route.uid)}` return '/' } function seoForRoute(route, profileDetail = null) { const teamName = route.page === 'team' ? canonicalTeamName(profileDetail, route.teamName).trim() || route.teamName : '' const teamSummary = profileDetail?.team_summary || profileDetail?.squadron_summary || null const teamPoints = Number(teamSummary?.points?.total_points || teamSummary?.total_points || 0) const teamBattles = Number(teamSummary?.total_battles || 0) if (route.page === 'team' && teamName) { const stats = [ teamPoints ? `${formatNumber(teamPoints)} points` : '', teamBattles ? `${formatNumber(teamBattles)} battles` : '', ].filter(Boolean) return { title: `${teamName} TSS Team Profile | Toothless' TSS Bot`, description: stats.length ? `${teamName} TSS team profile with ${stats.join(', ')}, roster details, battle history, and recent War Thunder squadron activity.` : `${teamName} TSS team profile with roster details, battle history, and recent War Thunder squadron activity.`, robots: 'index, follow', path: canonicalPathForRoute({ ...route, teamName }), } } if (route.page === 'player' && route.uid) { return { title: `Player ${route.uid} | Toothless' TSS Bot`, description: `TSS career stats for player ${route.uid} — battles, win rate, kills, and teams seen with.`, robots: 'noindex, follow', path: canonicalPathForRoute(route), } } if (route.page === 'game' && route.gameId) { return { title: `Game ${route.gameId} | Toothless' TSS Bot`, description: `TSS battle log details for game ${route.gameId}, including participants, map, player counts, and combat stats.`, robots: 'noindex, follow', path: canonicalPathForRoute(route), } } const byPage = { teams: { title: "TSS Team Leaderboard | Toothless' TSS Bot", description: 'Browse the live TSS team leaderboard, compare War Thunder squadron rankings, points, players, and recent team activity.', robots: 'index, follow', path: '/teams', }, 'battle-logs': { title: "TSS Battle Logs | Toothless' TSS Bot", description: 'Read recent TSS battle logs with team names, map history, player counts, battle times, and War Thunder match context.', robots: 'index, follow', path: '/battle-logs', }, uptime: { title: "TSS Bot Uptime Status | Toothless' TSS Bot", description: 'Check Toothless TSS Bot uptime, API health, TSS data proxy status, and recent availability history.', robots: 'index, follow', path: '/uptime', }, viewers: { title: "Viewer Analytics | Toothless' TSS Bot", description: 'Opt-in viewer analytics for Toothless TSS Bot, including active pages, broad device signals, and privacy-safe activity trends.', robots: 'noindex, follow', path: '/viewers', }, privacy: { title: "Privacy Notice | Toothless' TSS Bot", description: 'How Toothless TSS Bot handles cookies, opt-in analytics, viewer data, retention, deletion, and privacy rights.', robots: 'index, follow', path: '/privacy', }, docs: { title: "Docs | Toothless' TSS Bot", description: 'Documentation and command reference for Toothless TSS Bot.', robots: 'noindex, follow', path: '/docs', }, } return byPage[route.page] || { title: "Toothless' TSS Bot | Live TSS Leaderboards and Battle Logs", description: 'Live War Thunder TSS team leaderboards, battle logs, team profiles, uptime, and privacy-safe viewer analytics from Toothless TSS Bot.', robots: 'index, follow', path: '/', } } function upsertMeta(selector, createAttributes, valueAttribute, value) { let element = document.head.querySelector(selector) if (!element) { element = document.createElement('meta') Object.entries(createAttributes).forEach(([key, attributeValue]) => { element.setAttribute(key, attributeValue) }) document.head.appendChild(element) } element.setAttribute(valueAttribute, value) } function upsertLink(rel, href) { let element = document.head.querySelector(`link[rel="${rel}"]`) if (!element) { element = document.createElement('link') element.setAttribute('rel', rel) document.head.appendChild(element) } element.setAttribute('href', href) } function structuredDataForSeo(seo, canonicalUrl) { const base = currentPublicOrigin() const items = [ { '@context': 'https://schema.org', '@type': 'WebSite', name: "Toothless' TSS Bot", url: `${base}/`, description: "Live War Thunder TSS leaderboards, battle logs, and team profiles.", potentialAction: { '@type': 'SearchAction', target: `${base}/teams/{search_term_string}`, 'query-input': 'required name=search_term_string', }, }, { '@context': 'https://schema.org', '@type': 'WebPage', name: seo.title, url: canonicalUrl, description: seo.description, isPartOf: { '@type': 'WebSite', name: "Toothless' TSS Bot", url: `${base}/`, }, }, ] return JSON.stringify(items) } function applySeo(route, profileDetail = null) { const seo = seoForRoute(route, profileDetail) const canonicalUrl = `${currentPublicOrigin()}${seo.path}` document.title = seo.title upsertMeta('meta[name="description"]', { name: 'description' }, 'content', seo.description) upsertMeta('meta[name="robots"]', { name: 'robots' }, 'content', seo.robots) upsertMeta('meta[property="og:title"]', { property: 'og:title' }, 'content', seo.title) upsertMeta('meta[property="og:description"]', { property: 'og:description' }, 'content', seo.description) upsertMeta('meta[property="og:url"]', { property: 'og:url' }, 'content', canonicalUrl) upsertMeta('meta[name="twitter:title"]', { name: 'twitter:title' }, 'content', seo.title) upsertMeta('meta[name="twitter:description"]', { name: 'twitter:description' }, 'content', seo.description) upsertLink('canonical', canonicalUrl) let structuredData = document.getElementById('site-structured-data') if (!structuredData) { structuredData = document.createElement('script') structuredData.id = 'site-structured-data' structuredData.type = 'application/ld+json' document.head.appendChild(structuredData) } structuredData.textContent = structuredDataForSeo(seo, canonicalUrl) } async function fetchRecentTssGames(signal) { return fetchJson(apiEndpoints.recentGames, signal) } function Stat({ label, value }) { return (

{label}

{value}

) } function ThemeToggle({ theme, onThemeChange }) { const isDark = theme === 'dark' return ( ) } function defaultThemeTogglePosition() { const inset = window.innerWidth >= 640 ? 32 : 20 return { x: window.innerWidth - inset - 36, y: inset, } } function themeToggleDockPosition(navPill) { if (!navPill) return defaultThemeTogglePosition() const width = navPill.offsetWidth const height = navPill.offsetHeight const left = (window.innerWidth - width) / 2 const top = 16 return { x: Math.round(left + width - 48), y: Math.round(top + (height - 36) / 2), } } function ThemeToggleMover({ dockPosition, homePosition, isHome, theme, onThemeChange }) { const moverRef = useRef(null) useEffect(() => { const mover = moverRef.current if (!mover) return undefined let cancelled = false let tween = null async function animate() { const [{ gsap }, { ScrollTrigger }] = await Promise.all([ import('gsap'), import('gsap/ScrollTrigger'), ]) if (cancelled) return gsap.registerPlugin(ScrollTrigger) if (!isHome) { tween = gsap.to(mover, { x: dockPosition.x, y: dockPosition.y, duration: 0.56, ease: 'power3.out', overwrite: true, }) return } tween = gsap.fromTo( mover, { x: homePosition.x, y: homePosition.y }, { x: dockPosition.x, y: dockPosition.y, ease: 'none', overwrite: true, scrollTrigger: { end: '+=96', scrub: 0.35, start: 'top top', trigger: document.documentElement, }, }, ) } animate() return () => { cancelled = true tween?.scrollTrigger?.kill() tween?.kill() } }, [dockPosition.x, dockPosition.y, homePosition.x, homePosition.y, isHome]) const initialPosition = isHome ? homePosition : dockPosition return (
) } 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 embeddedGateState = document .querySelector('meta[name="tss-turnstile-session"]') ?.getAttribute('content') const initialGateState = turnstileSiteKey ? ['verified', 'required'].includes(embeddedGateState) ? embeddedGateState : 'checking' : 'verified' const [gateState, setGateState] = useState(initialGateState) useEffect(() => { if (!turnstileSiteKey || gateState !== 'checking') 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 } }, [gateState]) if (gateState === 'checking') { return