meow
This commit is contained in:
+254
-49
@@ -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' 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' 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>
|
||||
|
||||
Reference in New Issue
Block a user