ai generated solutions to our ai generated problems
This commit is contained in:
+131
-1
@@ -15,6 +15,7 @@ const apiEndpoints = {
|
||||
viewerEvent: '/api/viewers/event',
|
||||
viewerDelete: '/api/viewers/delete',
|
||||
teams: '/api/tss/leaderboard/teams?limit=100',
|
||||
players: '/api/tss/leaderboard/players?limit=100',
|
||||
homeTeams: '/api/tss/leaderboard/teams?limit=4',
|
||||
teamsHealth: '/api/tss/leaderboard/teams?limit=1',
|
||||
recentGames: '/api/tss/games/recent?limit=50',
|
||||
@@ -29,6 +30,7 @@ const apiEndpoints = {
|
||||
const navItems = [
|
||||
{ path: '/', label: 'Home' },
|
||||
{ path: '/teams', label: 'Team Leaderboard' },
|
||||
{ path: '/players', label: 'Player Leaderboard' },
|
||||
{ path: '/battle-logs', label: 'Battle Logs' },
|
||||
{ path: '/viewers', label: 'Viewers' },
|
||||
{ path: '/docs', label: 'Setup' },
|
||||
@@ -74,6 +76,7 @@ async function fetchJson(path, signal) {
|
||||
function parseRoute(pathname = window.location.pathname) {
|
||||
if (pathname === '/') return { page: 'home', teamName: '' }
|
||||
if (pathname === '/teams') return { page: 'teams', teamName: '' }
|
||||
if (pathname === '/players') return { page: 'players', teamName: '' }
|
||||
if (pathname === '/uptime') return { page: 'uptime', teamName: '' }
|
||||
if (pathname === '/viewers') return { page: 'viewers', teamName: '' }
|
||||
if (pathname === '/privacy') return { page: 'privacy', teamName: '' }
|
||||
@@ -102,6 +105,10 @@ function gamePath(gameId) {
|
||||
return `/games/${encodeURIComponent(gameId)}`
|
||||
}
|
||||
|
||||
function playerPath(uid) {
|
||||
return `/players/${encodeURIComponent(uid)}`
|
||||
}
|
||||
|
||||
function formatNumber(value) {
|
||||
return numberFormat.format(Number(value || 0))
|
||||
}
|
||||
@@ -420,6 +427,7 @@ function deviceType() {
|
||||
function routeLabel(route) {
|
||||
if (route.page === 'team' && route.teamName) return `Team: ${route.teamName}`
|
||||
if (route.page === 'teams') return 'Team Leaderboard'
|
||||
if (route.page === 'players') return 'Player 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'
|
||||
@@ -437,6 +445,7 @@ function currentPublicOrigin() {
|
||||
function canonicalPathForRoute(route) {
|
||||
if (route.page === 'team' && route.teamName) return teamPath(route.teamName)
|
||||
if (route.page === 'teams') return '/teams'
|
||||
if (route.page === 'players') return '/players'
|
||||
if (route.page === 'battle-logs') return '/battle-logs'
|
||||
if (route.page === 'game' && route.gameId) return gamePath(route.gameId)
|
||||
if (route.page === 'uptime') return '/uptime'
|
||||
@@ -496,6 +505,12 @@ function seoForRoute(route, profileDetail = null) {
|
||||
robots: 'index, follow',
|
||||
path: '/teams',
|
||||
},
|
||||
players: {
|
||||
title: "TSS Player Leaderboard | Toothless' TSS Bot",
|
||||
description: 'Browse the TSS player leaderboard, compare War Thunder player score, kills, win rate, KDR, and battle activity.',
|
||||
robots: 'index, follow',
|
||||
path: '/players',
|
||||
},
|
||||
'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.',
|
||||
@@ -912,6 +927,7 @@ function App() {
|
||||
function AppContent() {
|
||||
const [route, setRoute] = useState(() => parseRoute())
|
||||
const [leaderboard, setLeaderboard] = useState({ status: 'idle', data: null, error: null })
|
||||
const [playerLeaderboard, setPlayerLeaderboard] = useState({ status: 'idle', data: null, error: null })
|
||||
const [homeTeams, setHomeTeams] = useState({ status: 'idle', data: null, error: null })
|
||||
const [live, setLive] = useState({ status: 'idle', data: null, error: null, updatedAt: 0 })
|
||||
const [uptime, setUptime] = useState({ status: 'idle', checks: [], history: [], updatedAt: null })
|
||||
@@ -935,6 +951,10 @@ function AppContent() {
|
||||
() => leaderboard.data?.teams || leaderboard.data?.squadrons || [],
|
||||
[leaderboard.data],
|
||||
)
|
||||
const players = useMemo(
|
||||
() => playerLeaderboard.data?.players || [],
|
||||
[playerLeaderboard.data],
|
||||
)
|
||||
const teamsToWatch = useMemo(
|
||||
() => homeTeams.data?.teams || homeTeams.data?.squadrons || teams.slice(0, 4),
|
||||
[homeTeams.data, teams],
|
||||
@@ -1187,6 +1207,24 @@ function AppContent() {
|
||||
return () => controller.abort()
|
||||
}, [leaderboard.status, route.page])
|
||||
|
||||
useEffect(() => {
|
||||
if (route.page !== 'players') return
|
||||
if (playerLeaderboard.status === 'ready' || playerLeaderboard.status === 'loading') return
|
||||
|
||||
const controller = new AbortController()
|
||||
setPlayerLeaderboard({ status: 'loading', data: null, error: null })
|
||||
|
||||
fetchJson(apiEndpoints.players, controller.signal)
|
||||
.then((data) => setPlayerLeaderboard({ status: 'ready', data, error: null }))
|
||||
.catch((error) => {
|
||||
if (!controller.signal.aborted) {
|
||||
setPlayerLeaderboard({ status: 'error', data: null, error: error.message })
|
||||
}
|
||||
})
|
||||
|
||||
return () => controller.abort()
|
||||
}, [playerLeaderboard.status, route.page])
|
||||
|
||||
useEffect(() => {
|
||||
if (route.page !== 'home') return
|
||||
if (homeTeams.status === 'ready' || homeTeams.status === 'loading') return
|
||||
@@ -1558,6 +1596,8 @@ function AppContent() {
|
||||
const activeNavPath =
|
||||
route.page === 'team'
|
||||
? '/teams'
|
||||
: route.page === 'player' || route.page === 'players'
|
||||
? '/players'
|
||||
: route.page === 'battle-logs' || route.page === 'game'
|
||||
? '/battle-logs'
|
||||
: route.page === 'viewers'
|
||||
@@ -1586,6 +1626,26 @@ function AppContent() {
|
||||
}
|
||||
}, [route.page])
|
||||
|
||||
useEffect(() => {
|
||||
if (route.page !== 'players') return
|
||||
|
||||
const controller = new AbortController()
|
||||
const timer = window.setInterval(() => {
|
||||
fetchJson(apiEndpoints.players, controller.signal)
|
||||
.then((data) => setPlayerLeaderboard({ status: 'ready', data, error: null }))
|
||||
.catch((error) => {
|
||||
if (!controller.signal.aborted) {
|
||||
setPlayerLeaderboard((current) => ({ ...current, error: error.message }))
|
||||
}
|
||||
})
|
||||
}, 60000)
|
||||
|
||||
return () => {
|
||||
window.clearInterval(timer)
|
||||
controller.abort()
|
||||
}
|
||||
}, [route.page])
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-bg text-text">
|
||||
<header
|
||||
@@ -1654,6 +1714,9 @@ function AppContent() {
|
||||
{route.page === 'teams' ? (
|
||||
<TeamsPage leaderboard={leaderboard} navigate={navigate} teams={teams} />
|
||||
) : null}
|
||||
{route.page === 'players' ? (
|
||||
<PlayersPage leaderboard={playerLeaderboard} navigate={navigate} players={players} />
|
||||
) : null}
|
||||
{route.page === 'team' ? (
|
||||
<TeamProfilePage
|
||||
navigate={navigate}
|
||||
@@ -2723,6 +2786,73 @@ function TeamsPage({ leaderboard, navigate, teams }) {
|
||||
)
|
||||
}
|
||||
|
||||
function PlayersPage({ leaderboard, navigate, players }) {
|
||||
return (
|
||||
<section className="space-y-6 pt-24 sm:pt-28">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">Player Leaderboard</h1>
|
||||
<p className="mt-2 text-sm text-text-soft">
|
||||
{leaderboard.status === 'loading'
|
||||
? 'Loading player leaderboard'
|
||||
: leaderboard.error || `${players.length} players returned`}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="overflow-hidden rounded-lg border border-border bg-fury-white shadow-sm">
|
||||
<div className="overflow-x-auto">
|
||||
<div className="min-w-[900px]">
|
||||
{players.length ? (
|
||||
<div className="grid grid-cols-[4rem_minmax(220px,1fr)_repeat(7,92px)] gap-3 border-b border-surface px-5 py-3 text-xs font-semibold uppercase tracking-wide text-text-soft">
|
||||
<p>Rank</p>
|
||||
<p>Player</p>
|
||||
<p className="text-right">Score</p>
|
||||
<p className="text-right">Battles</p>
|
||||
<p className="text-right">Kills</p>
|
||||
<p className="text-right">Assists</p>
|
||||
<p className="text-right">WR</p>
|
||||
<p className="text-right">KDR</p>
|
||||
<p className="text-right">Teams</p>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{players.map((player, index) => (
|
||||
<button
|
||||
className="grid w-full grid-cols-[4rem_minmax(220px,1fr)_repeat(7,92px)] gap-3 border-b border-surface px-5 py-4 text-left text-sm transition hover:bg-surface"
|
||||
key={player.uid}
|
||||
onClick={() => navigate(playerPath(player.uid))}
|
||||
type="button"
|
||||
>
|
||||
<p className="font-semibold text-fury-cyan">#{index + 1}</p>
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-base font-semibold">{player.nick || player.uid}</p>
|
||||
<p className="truncate text-xs text-text-soft">
|
||||
{player.uid} · last seen {formatDate(player.last_seen)}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-right font-semibold">{formatNumber(player.score)}</p>
|
||||
<p className="text-right">{formatNumber(player.total_battles)}</p>
|
||||
<p className="text-right">{formatNumber(player.total_kills)}</p>
|
||||
<p className="text-right">{formatNumber(player.assists)}</p>
|
||||
<p className="text-right">{Number(player.win_rate || 0).toFixed(1)}%</p>
|
||||
<p className="text-right">{Number(player.kdr || 0).toFixed(2)}</p>
|
||||
<p className="text-right">{formatNumber(player.teams_seen)}</p>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!players.length ? (
|
||||
<p className="px-5 py-10 text-sm text-text-soft">
|
||||
{leaderboard.status === 'loading'
|
||||
? 'Loading player leaderboard'
|
||||
: leaderboard.error || 'No players returned'}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function TeamProfilePage({ navigate, profile, requestedTeam, teams }) {
|
||||
const detail = profile.detail.data
|
||||
const summary = detail?.team_summary || detail?.squadron_summary
|
||||
@@ -2872,7 +3002,7 @@ function GamePage({ gameId, navigate }) {
|
||||
<button
|
||||
className="grid w-full grid-cols-[minmax(220px,1fr)_80px_repeat(5,88px)] gap-3 px-5 py-2 text-left text-sm transition hover:bg-surface"
|
||||
key={player.uid}
|
||||
onClick={() => navigate(`/players/${encodeURIComponent(player.uid)}`)}
|
||||
onClick={() => navigate(playerPath(player.uid))}
|
||||
type="button"
|
||||
>
|
||||
<div className="min-w-0 pl-8 sm:pl-12">
|
||||
|
||||
Reference in New Issue
Block a user