meow
This commit is contained in:
+64
-29
@@ -33,12 +33,15 @@ const apiEndpoints = {
|
|||||||
searchPlayers: (name) => `/api/tss/players/search?q=${encodeURIComponent(name)}&limit=10`,
|
searchPlayers: (name) => `/api/tss/players/search?q=${encodeURIComponent(name)}&limit=10`,
|
||||||
}
|
}
|
||||||
|
|
||||||
const navItems = [
|
const primaryNavItems = [
|
||||||
{ path: '/', label: 'Home' },
|
{ path: '/', label: 'Home' },
|
||||||
{ path: '/teams', label: 'Team Leaderboard' },
|
{ path: '/teams', label: 'Team' },
|
||||||
{ path: '/players', label: 'Player Leaderboard' },
|
{ path: '/players', label: 'Players' },
|
||||||
{ path: '/battle-logs', label: 'Battle Logs' },
|
{ path: '/battle-logs', label: 'Battles' },
|
||||||
{ path: '/tournaments', label: 'Tournaments' },
|
{ path: '/tournaments', label: 'Tournaments' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const utilityNavItems = [
|
||||||
{ path: '/viewers', label: 'Viewers' },
|
{ path: '/viewers', label: 'Viewers' },
|
||||||
{ path: '/docs', label: 'Setup' },
|
{ path: '/docs', label: 'Setup' },
|
||||||
]
|
]
|
||||||
@@ -883,7 +886,7 @@ function themeToggleDockPosition(navPill) {
|
|||||||
const top = 16
|
const top = 16
|
||||||
|
|
||||||
return {
|
return {
|
||||||
x: Math.round(left + width - 48),
|
x: Math.round(left + width - 42),
|
||||||
y: Math.round(top + (height - 36) / 2),
|
y: Math.round(top + (height - 36) / 2),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1880,26 +1883,44 @@ function AppContent() {
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="flex max-w-[calc(100vw-2rem)] items-center gap-2 rounded-full border border-border bg-fury-white/92 py-2 pl-2 pr-13 shadow-[0_12px_36px_rgba(0,0,0,0.16)] backdrop-blur sm:pl-3"
|
className="flex max-w-[calc(100vw-2rem)] items-center gap-1.5 rounded-full border border-border bg-fury-white/92 py-1.5 pl-1.5 pr-11 shadow-[0_8px_24px_rgba(0,0,0,0.13)] backdrop-blur sm:pl-2"
|
||||||
ref={navPillRef}
|
ref={navPillRef}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
aria-label="Go to Toothless' TSS Bot home"
|
aria-label="Go to Toothless' TSS Bot home"
|
||||||
className="hidden h-10 w-10 shrink-0 place-items-center rounded-full transition hover:bg-surface sm:grid"
|
className="hidden h-8 w-8 shrink-0 place-items-center rounded-full transition hover:bg-surface sm:grid"
|
||||||
onClick={() => navigate('/')}
|
onClick={() => navigate('/')}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt=""
|
alt=""
|
||||||
className="h-8 w-8 rounded-full"
|
className="h-7 w-7 rounded-full"
|
||||||
src="/embed-icon.svg"
|
src="/embed-icon.svg"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<nav className="flex min-w-0 items-center gap-1 overflow-x-auto">
|
<nav className="flex min-w-0 items-center gap-0.5 overflow-x-auto">
|
||||||
{navItems.map((item) => (
|
{primaryNavItems.map((item) => (
|
||||||
<button
|
<button
|
||||||
className={`shrink-0 rounded-full px-3 py-2 text-sm font-semibold transition ${activeNavPath === item.path
|
className={`shrink-0 rounded-full px-2.5 py-1.5 text-[13px] font-semibold leading-5 transition sm:px-3 ${activeNavPath === item.path
|
||||||
|
? 'bg-text text-bg apricot-button-text'
|
||||||
|
: 'text-text-soft hover:bg-surface hover:text-text'
|
||||||
|
}`}
|
||||||
|
key={item.path}
|
||||||
|
onClick={() => navigate(item.path)}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div className="h-5 w-px shrink-0 bg-border/80" aria-hidden="true" />
|
||||||
|
|
||||||
|
<nav className="flex shrink-0 items-center gap-0.5">
|
||||||
|
{utilityNavItems.map((item) => (
|
||||||
|
<button
|
||||||
|
className={`shrink-0 rounded-full px-2.5 py-1.5 text-[13px] font-semibold leading-5 transition ${activeNavPath === item.path
|
||||||
? 'bg-text text-bg apricot-button-text'
|
? 'bg-text text-bg apricot-button-text'
|
||||||
: 'text-text-soft hover:bg-surface hover:text-text'
|
: 'text-text-soft hover:bg-surface hover:text-text'
|
||||||
}`}
|
}`}
|
||||||
@@ -2111,7 +2132,7 @@ function ConsentBanner({ preferences, onChoose }) {
|
|||||||
className="fixed inset-0 z-50 grid place-items-center overflow-y-auto bg-black/45 px-4 py-4 backdrop-blur-[2px] sm:py-6"
|
className="fixed inset-0 z-50 grid place-items-center overflow-y-auto bg-black/45 px-4 py-4 backdrop-blur-[2px] sm:py-6"
|
||||||
role="dialog"
|
role="dialog"
|
||||||
>
|
>
|
||||||
<div className="flex min-h-[18rem] max-h-[calc(100dvh-2rem)] w-full max-w-2xl resize-y flex-col overflow-hidden rounded-md border border-border bg-fury-white p-5 text-text shadow-[0_24px_70px_rgba(0,0,0,0.24)] sm:max-h-[calc(100dvh-3rem)] sm:p-6">
|
<div className="flex max-h-[calc(100dvh-2rem)] w-full max-w-2xl flex-col overflow-hidden rounded-md border border-border bg-fury-white p-5 text-text shadow-[0_24px_70px_rgba(0,0,0,0.24)] sm:max-h-[calc(100dvh-3rem)]">
|
||||||
<div className="max-w-xl shrink-0">
|
<div className="max-w-xl shrink-0">
|
||||||
<h2 id="consent-title" className="text-xl font-semibold">
|
<h2 id="consent-title" className="text-xl font-semibold">
|
||||||
Cookie and analytics settings
|
Cookie and analytics settings
|
||||||
@@ -2130,7 +2151,7 @@ function ConsentBanner({ preferences, onChoose }) {
|
|||||||
|
|
||||||
{isConfiguring ? (
|
{isConfiguring ? (
|
||||||
<>
|
<>
|
||||||
<div className="mt-5 min-h-0 space-y-3 overflow-y-auto pr-1">
|
<div className="mt-4 min-h-0 space-y-3 overflow-y-auto pr-1">
|
||||||
<PreferenceToggle
|
<PreferenceToggle
|
||||||
checked
|
checked
|
||||||
description="Stores this consent choice so the popup does not appear on every page."
|
description="Stores this consent choice so the popup does not appear on every page."
|
||||||
@@ -2180,7 +2201,7 @@ function ConsentBanner({ preferences, onChoose }) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-5 flex shrink-0 flex-col-reverse gap-2 sm:flex-row sm:justify-end">
|
<div className="mt-4 flex shrink-0 flex-col-reverse gap-2 sm:flex-row sm:justify-end">
|
||||||
<button
|
<button
|
||||||
className="rounded-md border border-border px-4 py-2 text-sm font-semibold text-text-soft transition hover:bg-surface hover:text-text"
|
className="rounded-md border border-border px-4 py-2 text-sm font-semibold text-text-soft transition hover:bg-surface hover:text-text"
|
||||||
onClick={declineAll}
|
onClick={declineAll}
|
||||||
@@ -2205,7 +2226,7 @@ function ConsentBanner({ preferences, onChoose }) {
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="mt-5 flex flex-col gap-2 sm:flex-row sm:justify-end">
|
<div className="mt-4 flex flex-col gap-2 sm:flex-row sm:justify-end">
|
||||||
<button
|
<button
|
||||||
className="rounded-md border border-border px-4 py-2 text-sm font-semibold text-text-soft transition hover:bg-surface hover:text-text"
|
className="rounded-md border border-border px-4 py-2 text-sm font-semibold text-text-soft transition hover:bg-surface hover:text-text"
|
||||||
onClick={declineAll}
|
onClick={declineAll}
|
||||||
@@ -2452,7 +2473,7 @@ function Landing({
|
|||||||
<PixelMountains />
|
<PixelMountains />
|
||||||
|
|
||||||
<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">
|
<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">
|
<div className="max-w-3xl lg:translate-y-[5px]">
|
||||||
<p className="text-base font-semibold uppercase tracking-wide text-fury-cyan">
|
<p className="text-base font-semibold uppercase tracking-wide text-fury-cyan">
|
||||||
BorisBot got nothin on THIS
|
BorisBot got nothin on THIS
|
||||||
</p>
|
</p>
|
||||||
@@ -2471,6 +2492,13 @@ function Landing({
|
|||||||
>
|
>
|
||||||
Team Leaderboard
|
Team Leaderboard
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
className="min-h-15 rounded-lg border-2 border-text bg-fury-white px-5 py-4 text-base font-semibold text-text transition hover:bg-surface"
|
||||||
|
onClick={() => navigate('/players')}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Player Leaderboard
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
className="min-h-15 rounded-lg border-2 border-ring px-5 py-4 text-base font-semibold text-fury-cyan transition hover:bg-surface hover:text-text"
|
className="min-h-15 rounded-lg border-2 border-ring px-5 py-4 text-base font-semibold text-fury-cyan transition hover:bg-surface hover:text-text"
|
||||||
onClick={() => navigate('/battle-logs')}
|
onClick={() => navigate('/battle-logs')}
|
||||||
@@ -2478,12 +2506,6 @@ function Landing({
|
|||||||
>
|
>
|
||||||
Battle Logs
|
Battle Logs
|
||||||
</button>
|
</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>
|
</div>
|
||||||
|
|
||||||
<form
|
<form
|
||||||
@@ -2516,9 +2538,9 @@ function Landing({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative min-h-[520px] overflow-hidden">
|
<div className="relative min-h-[500px] overflow-hidden">
|
||||||
<FallingLeaves treeRef={treeRef} />
|
<FallingLeaves treeRef={treeRef} />
|
||||||
<div className="absolute inset-0 z-[1] flex items-end justify-center pb-10 lg:pb-0">
|
<div className="absolute inset-0 z-[1] flex items-end justify-center">
|
||||||
<Tree ref={treeRef} />
|
<Tree ref={treeRef} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -3934,6 +3956,8 @@ function BracketViewport({ children }) {
|
|||||||
start.moved = true
|
start.moved = true
|
||||||
bracketPan.dragged = true
|
bracketPan.dragged = true
|
||||||
el.classList.add('is-grabbing')
|
el.classList.add('is-grabbing')
|
||||||
|
// Drop any text selection the initial press may have started.
|
||||||
|
window.getSelection()?.removeAllRanges()
|
||||||
}
|
}
|
||||||
if (start.moved) {
|
if (start.moved) {
|
||||||
el.scrollLeft = start.sl - dx
|
el.scrollLeft = start.sl - dx
|
||||||
@@ -3979,6 +4003,7 @@ function TournamentBracketSide({ side, navigate, highlight, onHover }) {
|
|||||||
// `tops` places each match; once placed we read back the DOM to draw connectors.
|
// `tops` places each match; once placed we read back the DOM to draw connectors.
|
||||||
const [layout, setLayout] = useState({ tops: new Map(), height: 0 })
|
const [layout, setLayout] = useState({ tops: new Map(), height: 0 })
|
||||||
const [connectors, setConnectors] = useState({ width: 0, height: 0, lines: [], byes: [] })
|
const [connectors, setConnectors] = useState({ width: 0, height: 0, lines: [], byes: [] })
|
||||||
|
const [collapsed, setCollapsed] = useState(false)
|
||||||
|
|
||||||
// Pass 1: measure card heights, then position every match centred on its feeders.
|
// Pass 1: measure card heights, then position every match centred on its feeders.
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
@@ -3994,7 +4019,7 @@ function TournamentBracketSide({ side, navigate, highlight, onHover }) {
|
|||||||
const observer = new ResizeObserver(relayout)
|
const observer = new ResizeObserver(relayout)
|
||||||
observer.observe(grid)
|
observer.observe(grid)
|
||||||
return () => observer.disconnect()
|
return () => observer.disconnect()
|
||||||
}, [side])
|
}, [side, collapsed])
|
||||||
|
|
||||||
// Pass 2: with matches positioned, read real edges and draw elbow connectors.
|
// Pass 2: with matches positioned, read real edges and draw elbow connectors.
|
||||||
// Each connector carries the teams on either end so a hovered team's whole run
|
// Each connector carries the teams on either end so a hovered team's whole run
|
||||||
@@ -4040,13 +4065,22 @@ function TournamentBracketSide({ side, navigate, highlight, onHover }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
setConnectors({ width: grid.scrollWidth, height: grid.scrollHeight, lines, byes })
|
setConnectors({ width: grid.scrollWidth, height: grid.scrollHeight, lines, byes })
|
||||||
}, [side, layout])
|
}, [side, layout, collapsed])
|
||||||
|
|
||||||
const active = Boolean(highlight)
|
const active = Boolean(highlight)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="bracket-side">
|
||||||
<h3 className="mb-3 text-sm font-semibold uppercase tracking-wide text-fury-violet">{side.label}</h3>
|
<button
|
||||||
|
aria-expanded={!collapsed}
|
||||||
|
className="mb-3 flex items-center gap-2 text-sm font-semibold uppercase tracking-wide text-fury-violet transition hover:text-text"
|
||||||
|
onClick={() => setCollapsed((value) => !value)}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span aria-hidden="true" className={`bracket-caret${collapsed ? ' is-collapsed' : ''}`}>▾</span>
|
||||||
|
{side.label}
|
||||||
|
</button>
|
||||||
|
{collapsed ? null : (
|
||||||
<BracketViewport>
|
<BracketViewport>
|
||||||
<div className={`tournament-bracket-grid${active ? ' has-trace' : ''}`} ref={gridRef}>
|
<div className={`tournament-bracket-grid${active ? ' has-trace' : ''}`} ref={gridRef}>
|
||||||
<svg
|
<svg
|
||||||
@@ -4101,6 +4135,7 @@ function TournamentBracketSide({ side, navigate, highlight, onHover }) {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</BracketViewport>
|
</BracketViewport>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -4263,7 +4298,7 @@ function TournamentDetailPage({ tournamentId, navigate }) {
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{bracketSides.length ? (
|
{bracketSides.length ? (
|
||||||
<div className="space-y-6">
|
<div className="space-y-12">
|
||||||
{hasStandings || listSides.length ? (
|
{hasStandings || listSides.length ? (
|
||||||
<h2 className="text-lg font-semibold">Playoffs</h2>
|
<h2 className="text-lg font-semibold">Playoffs</h2>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
+26
-3
@@ -834,20 +834,43 @@ h3 {
|
|||||||
/* The bracket reads as a campaign map you drag around. The viewport breaks the
|
/* The bracket reads as a campaign map you drag around. The viewport breaks the
|
||||||
page's max-width so there's room to manoeuvre; the surface carries a faint
|
page's max-width so there's room to manoeuvre; the surface carries a faint
|
||||||
tactical grid that pans with the bracket (it lives on the scrolling content). */
|
tactical grid that pans with the bracket (it lives on the scrolling content). */
|
||||||
|
/* The whole side (heading + canvas) breaks out to 85vw and centres on the
|
||||||
|
viewport, so the WINNER/LOSER heading aligns with the canvas's left edge. */
|
||||||
|
.bracket-side {
|
||||||
|
width: 85vw;
|
||||||
|
margin-left: calc(50% - 42.5vw);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bracket-caret {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 0.7em;
|
||||||
|
transition: transform 0.18s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bracket-caret.is-collapsed {
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
}
|
||||||
|
|
||||||
.bracket-viewport {
|
.bracket-viewport {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100vw;
|
width: 100%;
|
||||||
margin-left: calc(50% - 50vw);
|
|
||||||
max-height: 78vh;
|
max-height: 78vh;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
overscroll-behavior: contain;
|
overscroll-behavior: contain;
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
border-block: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: 0.5rem;
|
||||||
background-color: color-mix(in srgb, var(--color-bg) 86%, #000);
|
background-color: color-mix(in srgb, var(--color-bg) 86%, #000);
|
||||||
}
|
}
|
||||||
|
|
||||||
.bracket-viewport.is-grabbing {
|
.bracket-viewport.is-grabbing {
|
||||||
cursor: grabbing;
|
cursor: grabbing;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* While panning, don't let the drag turn into a text selection. */
|
||||||
|
.bracket-viewport.is-grabbing * {
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tournament-bracket-grid {
|
.tournament-bracket-grid {
|
||||||
|
|||||||
Reference in New Issue
Block a user