fuck it we ball
This commit is contained in:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user