This commit is contained in:
Heidi
2026-05-15 00:51:16 +01:00
parent 0f57bcdfc4
commit d40ecc99e0
2 changed files with 269 additions and 49 deletions
+254 -49
View File
@@ -354,6 +354,7 @@ function App() {
const [uptime, setUptime] = useState({ status: 'idle', checks: [], history: [], updatedAt: null })
const [viewers, setViewers] = useState({ status: 'idle', data: null, error: null, updatedAt: null })
const [analyticsPreferences, setAnalyticsPreferences] = useState(() => storedAnalyticsPreferences())
const [showFloatingNav, setShowFloatingNav] = useState(() => window.scrollY > 40)
const [teamQuery, setTeamQuery] = useState('')
const [searchHint, setSearchHint] = useState({ status: 'idle', name: '' })
const [profile, setProfile] = useState({
@@ -379,6 +380,13 @@ function App() {
return () => window.removeEventListener('popstate', onPopState)
}, [])
useEffect(() => {
const onScroll = () => setShowFloatingNav(window.scrollY > 40)
onScroll()
window.addEventListener('scroll', onScroll, { passive: true })
return () => window.removeEventListener('scroll', onScroll)
}, [])
useEffect(() => {
const title =
route.page === 'team' && route.teamName
@@ -870,18 +878,27 @@ function App() {
return (
<main className="min-h-screen bg-bg text-text">
<header className="sticky top-0 z-50 border-b border-border bg-bg/90 backdrop-blur">
<div className="mx-auto flex w-full max-w-7xl flex-col gap-3 px-5 py-3 sm:px-8 xl:flex-row xl:items-center">
<button className="shrink-0 text-left" onClick={() => navigate('/')} type="button">
<span className="text-lg font-bold tracking-tight">Toothless&apos; TSS Bot</span>
<header
className={`fixed inset-x-0 top-4 z-50 flex justify-center px-4 transition duration-300 ${showFloatingNav
? 'translate-y-0 opacity-100'
: 'pointer-events-none -translate-y-8 opacity-0'
}`}
>
<div className="flex max-w-[calc(100vw-2rem)] items-center gap-2 rounded-full border border-border bg-fury-white/92 px-2 py-2 shadow-[0_12px_36px_rgba(0,0,0,0.16)] backdrop-blur sm:px-3">
<button
className="hidden shrink-0 rounded-full px-3 py-2 text-sm font-bold tracking-tight transition hover:bg-surface sm:block"
onClick={() => navigate('/')}
type="button"
>
Toothless&apos; TSS Bot
</button>
<nav className="flex flex-wrap items-center gap-1 rounded-lg bg-surface/70 p-1 xl:ml-8">
<nav className="flex min-w-0 items-center gap-1 overflow-x-auto">
{navItems.map((item) => (
<button
className={`rounded-md px-3 py-2 text-sm font-semibold transition ${activeNavPath === item.path
className={`shrink-0 rounded-full px-3 py-2 text-sm font-semibold transition ${activeNavPath === item.path
? 'bg-text text-bg'
: 'text-text-soft hover:bg-fury-white'
: 'text-text-soft hover:bg-surface'
}`}
key={item.path}
onClick={() => navigate(item.path)}
@@ -890,34 +907,22 @@ function App() {
{item.label}
</button>
))}
<a
className="rounded-md px-3 py-2 text-sm font-semibold text-text-soft transition hover:bg-fury-white"
href="https://sre.pawjob.us/"
>
SRE
</a>
</nav>
<form className="flex min-w-0 gap-2 xl:ml-auto xl:w-[360px]" onSubmit={handleTeamSearch}>
<input
className="min-w-0 flex-1 rounded-md border border-border bg-fury-white px-3 py-2 text-sm outline-none transition focus:border-ring focus:shadow-[0_0_0_3px_var(--color-shadow)]"
placeholder={searchPlaceholder}
value={teamQuery}
onChange={(event) => setTeamQuery(event.target.value)}
/>
<button
className="rounded-md bg-fury-cyan px-4 py-2 text-sm font-semibold text-text transition hover:bg-fury-aqua"
type="submit"
>
Search
</button>
</form>
</div>
</header>
<section className="mx-auto w-full max-w-7xl px-5 sm:px-8">
{route.page === 'home' ? (
<Landing live={live} matches={matches} navigate={navigate} />
<Landing
live={live}
matches={matches}
navigate={navigate}
onTeamSearch={handleTeamSearch}
searchPlaceholder={searchPlaceholder}
setTeamQuery={setTeamQuery}
teams={teams}
teamQuery={teamQuery}
/>
) : null}
{route.page === 'teams' ? (
<TeamsPage leaderboard={leaderboard} navigate={navigate} teams={teams} />
@@ -1263,15 +1268,24 @@ function PrivacySection({ children, title }) {
)
}
function Landing({ live, matches, navigate }) {
function Landing({
live,
matches,
navigate,
onTeamSearch,
searchPlaceholder,
setTeamQuery,
teams,
teamQuery,
}) {
const treeRef = useRef(null)
return (
<div className="relative left-1/2 w-screen -translate-x-1/2 bg-bg">
<div className="relative min-h-[calc(100vh-74px)] overflow-hidden px-5 sm:px-8">
<div className="relative min-h-screen overflow-hidden px-5 sm:px-8">
<PixelMountains />
<section className="relative z-10 mx-auto grid min-h-[calc(100vh-74px)] w-full max-w-7xl gap-8 pt-16 pb-10 lg:grid-cols-[1.05fr_0.95fr] lg:items-center">
<section className="relative z-10 mx-auto grid min-h-screen w-full max-w-7xl gap-8 pt-16 pb-16 lg:grid-cols-[1.05fr_0.95fr] lg:items-center">
<div className="max-w-3xl">
<p className="text-base font-semibold uppercase tracking-wide text-fury-cyan">
BorisBot got nothin on THIS
@@ -1282,21 +1296,47 @@ function Landing({ live, matches, navigate }) {
<p className="mt-6 max-w-2xl text-xl leading-9 text-text-soft">
Powered by Spectra. TSS analytics.
</p>
<div className="mt-8 flex flex-wrap gap-4">
<button
className="rounded-lg bg-text px-7 py-4 text-base font-semibold text-bg"
onClick={() => navigate('/teams')}
type="button"
<div className="mt-8 w-full max-w-xl">
<div className="grid gap-4 sm:grid-cols-3">
<button
className="min-h-15 rounded-lg bg-text px-5 py-4 text-base font-semibold text-bg"
onClick={() => navigate('/teams')}
type="button"
>
Team leaderboard
</button>
<button
className="min-h-15 rounded-lg border-2 border-ring px-5 py-4 text-base font-semibold text-fury-cyan"
onClick={() => navigate('/battle-logs')}
type="button"
>
Battle Logs
</button>
<a
className="flex min-h-15 items-center justify-center rounded-lg border-2 border-text bg-fury-white px-5 py-4 text-base font-semibold text-text transition hover:bg-surface"
href="https://sre.pawjob.us/"
>
Visit SREBOT
</a>
</div>
<form
className="mt-5 grid gap-2 rounded-lg border border-border bg-fury-white/88 p-2 shadow-sm backdrop-blur sm:grid-cols-[1fr_auto]"
onSubmit={onTeamSearch}
>
Team leaderboard
</button>
<button
className="rounded-lg border-2 border-ring px-7 py-4 text-base font-semibold text-fury-cyan"
onClick={() => navigate('/battle-logs')}
type="button"
>
Battle Logs
</button>
<input
className="min-w-0 rounded-md border border-border bg-bg px-4 py-3 text-sm outline-none transition focus:border-ring focus:shadow-[0_0_0_3px_var(--color-shadow)]"
placeholder={searchPlaceholder}
value={teamQuery}
onChange={(event) => setTeamQuery(event.target.value)}
/>
<button
className="rounded-md bg-fury-cyan px-5 py-3 text-sm font-semibold text-text transition hover:bg-fury-aqua"
type="submit"
>
Search teams
</button>
</form>
</div>
</div>
@@ -1306,14 +1346,179 @@ function Landing({ live, matches, navigate }) {
<Tree ref={treeRef} />
</div>
</div>
<div className="absolute bottom-5 left-1/2 z-20 hidden -translate-x-1/2 flex-col items-center gap-2 text-xs font-semibold uppercase tracking-wide text-text-soft sm:flex">
<span>Scroll</span>
<span className="h-10 w-[2px] overflow-hidden rounded-full bg-border">
<span className="block h-4 w-full animate-[scrollPulse_1.5s_ease-in-out_infinite] rounded-full bg-fury-cyan" />
</span>
</div>
</section>
</div>
<LandingOverview teams={teams} matches={matches} navigate={navigate} />
<RecentGamesSection live={live} matches={matches} navigate={navigate} />
<LandingTrustSection navigate={navigate} />
</div>
)
}
function LandingOverview({ teams, matches, navigate }) {
const activeTeams = teams.slice(0, 4)
const totalPlayers = matches.reduce((sum, match) => sum + Number(match.player_count || 0), 0)
const latestMatch = matches[0]
return (
<section className="relative z-20 border-t border-border bg-bg px-5 py-12 sm:px-8">
<div className="mx-auto grid w-full max-w-7xl gap-10 lg:grid-cols-[0.9fr_1.1fr] lg:items-start">
<div>
<p className="text-sm font-semibold uppercase tracking-wide text-fury-cyan">
What it does
</p>
<h2 className="mt-2 text-3xl font-bold">A clean window into TSS activity</h2>
<p className="mt-4 max-w-2xl text-base leading-7 text-text-soft">
Track teams, inspect recent battles, and jump from a leaderboard name to a
profile without touching the god awful TSS website.
</p>
<div className="mt-6 grid gap-3 sm:grid-cols-3">
<LandingMetric label="Teams indexed" value={formatNumber(teams.length)} />
<LandingMetric label="Recent games" value={formatNumber(matches.length)} />
<LandingMetric label="Players seen" value={formatNumber(totalPlayers)} />
</div>
</div>
<div className="grid gap-4 md:grid-cols-2">
<LandingFeature
action="Open leaderboard"
description="We track teams so you don't have to."
onClick={() => navigate('/teams')}
title="Team discovery"
/>
<LandingFeature
action="Read battle logs"
description="Recent games, map names, player counts, and combat stat summaries."
onClick={() => navigate('/battle-logs')}
title="Battle context"
/>
<LandingFeature
action="Check uptime"
description="Track if TSS data not showing up is our fault or Gaijins, wink wink."
onClick={() => navigate('/uptime')}
title="Operational status"
/>
<LandingFeature
action="View analytics"
description="Track what other people are watching utilising opt in data analytics."
onClick={() => navigate('/viewers')}
title="Viewer pulse"
/>
</div>
</div>
<div className="mx-auto mt-10 grid w-full max-w-7xl gap-4 lg:grid-cols-[1fr_1fr]">
<div className="border-l border-border bg-fury-white px-5 py-5">
<p className="text-sm font-semibold uppercase tracking-wide text-fury-cyan">
Latest signal
</p>
<h3 className="mt-2 text-2xl font-bold">
{latestMatch?.map_name || 'Waiting for the next battle'}
</h3>
<p className="mt-2 text-sm leading-6 text-text-soft">
{latestMatch
? `${latestMatch.team_name || 'A TSS team'} played with ${formatNumber(latestMatch.player_count)} players.`
: 'Latest match data will appear here once the data proxy returns games.'}
</p>
</div>
<div className="border-l border-border bg-fury-white px-5 py-5">
<p className="text-sm font-semibold uppercase tracking-wide text-fury-cyan">
Teams to watch
</p>
<div className="mt-3 grid gap-2 sm:grid-cols-2">
{activeTeams.length ? (
activeTeams.map((team) => {
const name = bestTeamName(team)
return (
<button
className="truncate rounded-md border border-border bg-bg px-3 py-2 text-left text-sm font-semibold transition hover:border-ring hover:bg-surface"
key={name}
onClick={() => navigate(teamPath(name))}
type="button"
>
{name}
</button>
)
})
) : (
<p className="text-sm text-text-soft">Team data is still loading.</p>
)}
</div>
</div>
</div>
</section>
)
}
function LandingMetric({ label, value }) {
return (
<div className="border-l border-border pl-4">
<p className="text-2xl font-bold text-text">{value}</p>
<p className="mt-1 text-xs font-semibold uppercase tracking-wide text-text-soft">{label}</p>
</div>
)
}
function LandingFeature({ action, description, onClick, title }) {
return (
<article className="rounded-lg border border-border bg-fury-white p-5 shadow-sm">
<h3 className="text-lg font-semibold">{title}</h3>
<p className="mt-2 min-h-12 text-sm leading-6 text-text-soft">{description}</p>
<button
className="mt-4 text-sm font-semibold text-fury-cyan transition hover:text-text"
onClick={onClick}
type="button"
>
{action}
</button>
</article>
)
}
function LandingTrustSection({ navigate }) {
return (
<section className="relative z-20 border-t border-border bg-bg px-5 py-10 sm:px-8">
<div className="mx-auto grid w-full max-w-7xl gap-6 lg:grid-cols-[1.2fr_0.8fr] lg:items-center">
<div>
<p className="text-sm font-semibold uppercase tracking-wide text-fury-cyan">
Brought to you by...
</p>
<h2 className="mt-2 text-3xl font-bold">Built by the team behind SREBot</h2>
<p className="mt-3 max-w-3xl text-base leading-7 text-text-soft">
TSSBot is built alongside SREBot, both providing different views into different aspects of "competitive" War Thunder.
</p>
</div>
<div className="flex flex-wrap gap-3 lg:justify-end">
<a
className="rounded-lg bg-text px-5 py-3 text-sm font-semibold text-bg transition hover:bg-fury-cyan"
href="https://sre.pawjob.us/"
>
Visit SREBOT
</a>
<button
className="rounded-lg border border-ring px-5 py-3 text-sm font-semibold text-fury-cyan transition hover:bg-surface"
onClick={() => navigate('/privacy')}
type="button"
>
Privacy notice
</button>
</div>
</div>
</section>
)
}
function RecentGamesSection({ live, matches, navigate }) {
const recentMatches = matches.slice(0, 6)
@@ -1498,7 +1703,7 @@ function PixelMountains() {
function TeamsPage({ leaderboard, navigate, teams }) {
return (
<section className="space-y-6">
<section className="space-y-6 pt-10 sm:pt-14">
<div>
<h1 className="text-3xl font-bold">Team leaderboard</h1>
<p className="mt-2 text-sm text-text-soft">
@@ -1741,7 +1946,7 @@ function BattleResults({ games, status }) {
function BattleLogsPage({ live, matches }) {
return (
<section className="space-y-6">
<section className="space-y-6 pt-10 sm:pt-14">
<div>
<h1 className="text-3xl font-bold">Battle Logs</h1>
<p className="mt-2 text-sm text-text-soft">
@@ -1808,7 +2013,7 @@ function ViewersPage({ viewers }) {
const generatedAt = data.generated_at ? dateFormat.format(new Date(data.generated_at)) : 'Waiting for data'
return (
<section className="space-y-6">
<section className="space-y-6 pt-10 sm:pt-14">
<div className="rounded-lg border border-border bg-fury-white p-6 shadow-sm">
<div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
<div>