fuck it we ball

This commit is contained in:
FURRO404
2026-05-30 08:44:28 -07:00
parent 64c72d2ecb
commit 3436c91fdc
3 changed files with 408 additions and 0 deletions
+104
View File
@@ -24,6 +24,7 @@ const apiEndpoints = {
detail: (name) => `/api/tss/teams/${encodeURIComponent(name)}`,
history: (name) => `/api/tss/teams/${encodeURIComponent(name)}/history`,
games: (name) => `/api/tss/teams/${encodeURIComponent(name)}/games`,
player: (uid) => `/api/tss/player/${encodeURIComponent(uid)}`,
}
const navItems = [
@@ -77,6 +78,10 @@ function parseRoute(pathname = window.location.pathname) {
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('/teams/')) {
const teamName = decodeURIComponent(pathname.slice('/teams/'.length))
return { page: 'team', teamName }
@@ -356,6 +361,7 @@ function routeLabel(route) {
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'
}
@@ -371,6 +377,7 @@ function canonicalPathForRoute(route) {
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 '/'
}
@@ -398,6 +405,15 @@ function seoForRoute(route, profileDetail = null) {
}
}
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),
}
}
const byPage = {
teams: {
title: "TSS Team Leaderboard | Toothless' TSS Bot",
@@ -1602,6 +1618,7 @@ function AppContent() {
{route.page === 'viewers' ? <ViewersPage viewers={viewers} /> : null}
{route.page === 'privacy' ? <PrivacyPage /> : null}
{route.page === 'docs' ? <DocsPage /> : null}
{route.page === 'player' ? <PlayerPage uid={route.uid} navigate={navigate} /> : null}
</section>
<Footer navigate={navigate} />
<ConsentBanner preferences={analyticsPreferences} onChoose={chooseAnalyticsConsent} />
@@ -1945,6 +1962,93 @@ function PrivacyPage() {
)
}
function PlayerStatCard({ label, value }) {
return (
<div className="rounded-xl border border-border bg-fury-white p-4">
<p className="text-xs font-semibold uppercase tracking-wide text-text-soft">{label}</p>
<p className="mt-1 text-2xl font-bold">{value}</p>
</div>
)
}
function PlayerPage({ uid, navigate }) {
const [state, setState] = useState({ status: 'loading', data: null, error: '' })
useEffect(() => {
if (!uid) {
setState({ status: 'error', data: null, error: 'No player specified.' })
return
}
let cancelled = false
setState({ status: 'loading', data: null, error: '' })
fetchJson(apiEndpoints.player(uid))
.then((data) => {
if (!cancelled) setState({ status: 'ready', data, error: '' })
})
.catch((err) => {
if (!cancelled) setState({ status: 'error', data: null, error: err?.message || 'Failed to load player.' })
})
return () => {
cancelled = true
}
}, [uid])
const { status, data, error } = state
return (
<section className="mx-auto max-w-4xl pb-12 pt-24 sm:pt-28">
<div className="border-b border-border pb-6">
<p className="text-sm font-semibold uppercase tracking-wide text-fury-cyan">Player</p>
<h1 className="mt-2 text-4xl font-bold">
{status === 'ready' ? (data.nick || `Player ${uid}`) : `Player ${uid || ''}`}
</h1>
<p className="mt-3 text-text-soft">UID {uid} · TSS career</p>
</div>
{status === 'loading' ? <p className="mt-8 text-text-soft">Loading</p> : null}
{status === 'error' ? <p className="mt-8 text-text-soft">{error}</p> : null}
{status === 'ready' ? (
<div className="mt-8 grid gap-8">
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3">
<PlayerStatCard label="Battles" value={formatNumber(data.career.total_battles)} />
<PlayerStatCard label="Win rate" value={`${(data.career.win_rate || 0).toFixed(1)}%`} />
<PlayerStatCard label="K/D" value={(data.career.kdr || 0).toFixed(2)} />
<PlayerStatCard label="Total kills" value={formatNumber(data.career.total_kills)} />
<PlayerStatCard label="Wins / Losses" value={`${formatNumber(data.career.wins)} / ${formatNumber(data.career.losses)}`} />
<PlayerStatCard label="Deaths" value={formatNumber(data.career.deaths)} />
<PlayerStatCard label="Ground kills" value={formatNumber(data.career.ground_kills)} />
<PlayerStatCard label="Air kills" value={formatNumber(data.career.air_kills)} />
<PlayerStatCard label="Assists" value={formatNumber(data.career.assists)} />
</div>
{Array.isArray(data.teams) && data.teams.length ? (
<div>
<h2 className="text-lg font-semibold text-text">Teams seen with</h2>
<div className="mt-3 flex flex-col gap-2">
{data.teams.map((team) => {
const label = team.team_tag || team.team_name || 'Unknown'
return (
<button
key={`${team.team_tag}-${team.team_id ?? ''}`}
className="flex items-center justify-between rounded-lg border border-border bg-fury-white px-4 py-3 text-left transition hover:border-fury-cyan"
onClick={() => navigate(teamPath(label))}
type="button"
>
<span className="font-semibold text-text">{label}</span>
<span className="text-sm text-text-soft">{formatNumber(team.games)} games</span>
</button>
)
})}
</div>
</div>
) : null}
</div>
) : null}
</section>
)
}
function DocsPage() {
return (
<section className="mx-auto max-w-4xl pb-12 pt-24 sm:pt-28">