ai generated solutions to our ai generated problems
This commit is contained in:
+30
-5
@@ -386,14 +386,12 @@ async fn leaderboard(
|
|||||||
return Ok(Json(LeaderboardResponse { teams }));
|
return Ok(Json(LeaderboardResponse { teams }));
|
||||||
}
|
}
|
||||||
|
|
||||||
let rows = leaderboard_rows(&state, limit)?;
|
let teams = leaderboard_roster_rows(&state, limit)?;
|
||||||
cache_leaderboard(&state, &rows)?;
|
Ok(Json(LeaderboardResponse { teams }))
|
||||||
Ok(Json(LeaderboardResponse { teams: rows }))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn leaderboard_rows(state: &AppState, limit: usize) -> Result<Vec<TeamLeaderboardRow>, ApiError> {
|
fn leaderboard_teams(state: &AppState, limit: usize) -> Result<Vec<TeamRecord>, ApiError> {
|
||||||
let teams_conn = open_db(&state.teams_db)?;
|
let teams_conn = open_db(&state.teams_db)?;
|
||||||
let battles_conn = open_db(&state.battles_db)?;
|
|
||||||
// Deduplicate teams by name across tournaments — pick the highest team_id
|
// Deduplicate teams by name across tournaments — pick the highest team_id
|
||||||
// (most recent) per name for the roster count, but stats come from team_name.
|
// (most recent) per name for the roster count, but stats come from team_name.
|
||||||
let mut stmt = teams_conn
|
let mut stmt = teams_conn
|
||||||
@@ -411,6 +409,33 @@ fn leaderboard_rows(state: &AppState, limit: usize) -> Result<Vec<TeamLeaderboar
|
|||||||
.map_err(db_error)?
|
.map_err(db_error)?
|
||||||
.collect::<Result<Vec<_>, _>>()
|
.collect::<Result<Vec<_>, _>>()
|
||||||
.map_err(db_error)?;
|
.map_err(db_error)?;
|
||||||
|
Ok(teams)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn leaderboard_roster_rows(
|
||||||
|
state: &AppState,
|
||||||
|
limit: usize,
|
||||||
|
) -> Result<Vec<TeamLeaderboardRow>, ApiError> {
|
||||||
|
leaderboard_teams(state, limit).map(|teams| {
|
||||||
|
teams
|
||||||
|
.into_iter()
|
||||||
|
.map(|team| TeamLeaderboardRow {
|
||||||
|
team_id: team.team_id,
|
||||||
|
name: team.name,
|
||||||
|
player_count: team.members,
|
||||||
|
total_battles: 0,
|
||||||
|
wins: 0,
|
||||||
|
losses: 0,
|
||||||
|
win_rate: 0.0,
|
||||||
|
total_kills: 0,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn leaderboard_rows(state: &AppState, limit: usize) -> Result<Vec<TeamLeaderboardRow>, ApiError> {
|
||||||
|
let battles_conn = open_db(&state.battles_db)?;
|
||||||
|
let teams = leaderboard_teams(state, limit)?;
|
||||||
let team_names = teams
|
let team_names = teams
|
||||||
.iter()
|
.iter()
|
||||||
.map(|team| team.name.as_str())
|
.map(|team| team.name.as_str())
|
||||||
|
|||||||
+60
-53
@@ -1,9 +1,13 @@
|
|||||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
|
||||||
import L from 'leaflet'
|
import L from 'leaflet'
|
||||||
|
import { gsap } from 'gsap'
|
||||||
|
import { ScrollTrigger } from 'gsap/ScrollTrigger'
|
||||||
import 'leaflet/dist/leaflet.css'
|
import 'leaflet/dist/leaflet.css'
|
||||||
import Tree, { prewarmTreeCanvas } from '../Tree/Tree'
|
import Tree, { prewarmTreeCanvas } from '../Tree/Tree'
|
||||||
import FallingLeaves from '../Tree/FallingLeaves'
|
import FallingLeaves from '../Tree/FallingLeaves'
|
||||||
|
|
||||||
|
gsap.registerPlugin(ScrollTrigger)
|
||||||
|
|
||||||
const numberFormat = new Intl.NumberFormat('en-GB')
|
const numberFormat = new Intl.NumberFormat('en-GB')
|
||||||
const dateFormat = new Intl.DateTimeFormat('en-GB', {
|
const dateFormat = new Intl.DateTimeFormat('en-GB', {
|
||||||
dateStyle: 'medium',
|
dateStyle: 'medium',
|
||||||
@@ -593,53 +597,51 @@ function themeToggleDockPosition(navPill) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function ThemeToggleMover({ position, theme, onThemeChange }) {
|
function ThemeToggleMover({ dockPosition, homePosition, isHome, theme, onThemeChange }) {
|
||||||
const previousPositionRef = useRef(position)
|
const moverRef = useRef(null)
|
||||||
const [motion, setMotion] = useState(() => ({
|
|
||||||
from: position,
|
|
||||||
to: position,
|
|
||||||
mid: position,
|
|
||||||
key: 0,
|
|
||||||
animate: false,
|
|
||||||
}))
|
|
||||||
|
|
||||||
useEffect(() => {
|
useLayoutEffect(() => {
|
||||||
const from = previousPositionRef.current
|
const mover = moverRef.current
|
||||||
const to = position
|
if (!mover) return undefined
|
||||||
if (from.x === to.x && from.y === to.y) return
|
|
||||||
|
|
||||||
const distance = Math.hypot(to.x - from.x, to.y - from.y)
|
if (!isHome) {
|
||||||
const lift = Math.min(96, Math.max(24, distance * 0.16))
|
gsap.to(mover, {
|
||||||
const mid = {
|
x: dockPosition.x,
|
||||||
x: Math.round((from.x + to.x) / 2),
|
y: dockPosition.y,
|
||||||
y: Math.round(Math.min(from.y, to.y) - lift),
|
duration: 0.56,
|
||||||
|
ease: 'power3.out',
|
||||||
|
overwrite: true,
|
||||||
|
})
|
||||||
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
previousPositionRef.current = to
|
const tween = gsap.fromTo(
|
||||||
setMotion((current) => ({
|
mover,
|
||||||
from,
|
{ x: homePosition.x, y: homePosition.y },
|
||||||
to,
|
{
|
||||||
mid,
|
x: dockPosition.x,
|
||||||
key: current.key + 1,
|
y: dockPosition.y,
|
||||||
animate: true,
|
ease: 'none',
|
||||||
}))
|
overwrite: true,
|
||||||
}, [position])
|
scrollTrigger: {
|
||||||
|
end: '+=96',
|
||||||
|
scrub: 0.35,
|
||||||
|
start: 'top top',
|
||||||
|
trigger: document.documentElement,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
tween.scrollTrigger?.kill()
|
||||||
|
tween.kill()
|
||||||
|
}
|
||||||
|
}, [dockPosition.x, dockPosition.y, homePosition.x, homePosition.y, isHome])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`theme-toggle-mover fixed left-0 top-0 z-[60] ${
|
className="theme-toggle-mover fixed left-0 top-0 z-[60]"
|
||||||
motion.animate ? 'theme-toggle-mover-arc' : ''
|
ref={moverRef}
|
||||||
}`}
|
|
||||||
key={motion.key}
|
|
||||||
style={{
|
|
||||||
'--from-x': `${motion.from.x}px`,
|
|
||||||
'--from-y': `${motion.from.y}px`,
|
|
||||||
'--mid-x': `${motion.mid.x}px`,
|
|
||||||
'--mid-y': `${motion.mid.y}px`,
|
|
||||||
'--to-x': `${motion.to.x}px`,
|
|
||||||
'--to-y': `${motion.to.y}px`,
|
|
||||||
transform: `translate3d(${motion.to.x}px, ${motion.to.y}px, 0)`,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<ThemeToggle theme={theme} onThemeChange={onThemeChange} />
|
<ThemeToggle theme={theme} onThemeChange={onThemeChange} />
|
||||||
</div>
|
</div>
|
||||||
@@ -821,7 +823,10 @@ function AppContent() {
|
|||||||
const [analyticsPreferences, setAnalyticsPreferences] = useState(() => storedAnalyticsPreferences())
|
const [analyticsPreferences, setAnalyticsPreferences] = useState(() => storedAnalyticsPreferences())
|
||||||
const [theme, setTheme] = useState(() => storedThemePreference())
|
const [theme, setTheme] = useState(() => storedThemePreference())
|
||||||
const [showFloatingNav, setShowFloatingNav] = useState(() => window.scrollY > 40)
|
const [showFloatingNav, setShowFloatingNav] = useState(() => window.scrollY > 40)
|
||||||
const [themeTogglePosition, setThemeTogglePosition] = useState(() => defaultThemeTogglePosition())
|
const [themeTogglePositions, setThemeTogglePositions] = useState(() => {
|
||||||
|
const position = defaultThemeTogglePosition()
|
||||||
|
return { dock: position, home: position }
|
||||||
|
})
|
||||||
const [teamQuery, setTeamQuery] = useState('')
|
const [teamQuery, setTeamQuery] = useState('')
|
||||||
const [searchHint, setSearchHint] = useState({ status: 'idle', name: '' })
|
const [searchHint, setSearchHint] = useState({ status: 'idle', name: '' })
|
||||||
const [teamSearchResults, setTeamSearchResults] = useState([])
|
const [teamSearchResults, setTeamSearchResults] = useState([])
|
||||||
@@ -1476,24 +1481,24 @@ function AppContent() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let frame = 0
|
let frame = 0
|
||||||
|
|
||||||
function updateThemeTogglePosition() {
|
function updateThemeTogglePositions() {
|
||||||
window.cancelAnimationFrame(frame)
|
window.cancelAnimationFrame(frame)
|
||||||
frame = window.requestAnimationFrame(() => {
|
frame = window.requestAnimationFrame(() => {
|
||||||
setThemeTogglePosition(
|
setThemeTogglePositions({
|
||||||
shouldShowFloatingNav
|
dock: themeToggleDockPosition(navPillRef.current),
|
||||||
? themeToggleDockPosition(navPillRef.current)
|
home: defaultThemeTogglePosition(),
|
||||||
: defaultThemeTogglePosition(),
|
})
|
||||||
)
|
ScrollTrigger.refresh()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
updateThemeTogglePosition()
|
updateThemeTogglePositions()
|
||||||
window.addEventListener('resize', updateThemeTogglePosition)
|
window.addEventListener('resize', updateThemeTogglePositions)
|
||||||
return () => {
|
return () => {
|
||||||
window.cancelAnimationFrame(frame)
|
window.cancelAnimationFrame(frame)
|
||||||
window.removeEventListener('resize', updateThemeTogglePosition)
|
window.removeEventListener('resize', updateThemeTogglePositions)
|
||||||
}
|
}
|
||||||
}, [route.page, shouldShowFloatingNav])
|
}, [route.page])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="min-h-screen bg-bg text-text">
|
<main className="min-h-screen bg-bg text-text">
|
||||||
@@ -1533,7 +1538,9 @@ function AppContent() {
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<ThemeToggleMover
|
<ThemeToggleMover
|
||||||
position={themeTogglePosition}
|
dockPosition={themeTogglePositions.dock}
|
||||||
|
homePosition={themeTogglePositions.home}
|
||||||
|
isHome={route.page === 'home'}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
onThemeChange={chooseTheme}
|
onThemeChange={chooseTheme}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+2
-16
@@ -477,8 +477,8 @@ h3 {
|
|||||||
0 1px 3px rgba(0, 0, 0, 0.35);
|
0 1px 3px rgba(0, 0, 0, 0.35);
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-toggle-mover-arc {
|
.theme-toggle-mover {
|
||||||
animation: themeToggleArc 560ms cubic-bezier(0.22, 1, 0.36, 1) forwards;
|
will-change: transform;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root.theme-transition,
|
:root.theme-transition,
|
||||||
@@ -535,20 +535,6 @@ h3 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes themeToggleArc {
|
|
||||||
0% {
|
|
||||||
transform: translate3d(var(--from-x), var(--from-y), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
50% {
|
|
||||||
transform: translate3d(var(--mid-x), var(--mid-y), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
transform: translate3d(var(--to-x), var(--to-y), 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes celestialPathExit {
|
@keyframes celestialPathExit {
|
||||||
0% {
|
0% {
|
||||||
offset-distance: 0%;
|
offset-distance: 0%;
|
||||||
|
|||||||
Generated
+7
@@ -11,6 +11,7 @@
|
|||||||
"@tailwindcss/vite": "^4.1.8",
|
"@tailwindcss/vite": "^4.1.8",
|
||||||
"@vitejs/plugin-react": "^4.5.0",
|
"@vitejs/plugin-react": "^4.5.0",
|
||||||
"better-sqlite3": "^12.10.0",
|
"better-sqlite3": "^12.10.0",
|
||||||
|
"gsap": "^3.15.0",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
@@ -3130,6 +3131,12 @@
|
|||||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/gsap": {
|
||||||
|
"version": "3.15.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/gsap/-/gsap-3.15.0.tgz",
|
||||||
|
"integrity": "sha512-dMW4CWBTUK1AEEDeZc1g4xpPGIrSf9fJF960qbTZmN/QwZIWY5wgliS6JWl9/25fpTGJrMRtSjGtOmPnfjZB+A==",
|
||||||
|
"license": "Standard 'no charge' license: https://gsap.com/standard-license."
|
||||||
|
},
|
||||||
"node_modules/has-flag": {
|
"node_modules/has-flag": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
"@tailwindcss/vite": "^4.1.8",
|
"@tailwindcss/vite": "^4.1.8",
|
||||||
"@vitejs/plugin-react": "^4.5.0",
|
"@vitejs/plugin-react": "^4.5.0",
|
||||||
"better-sqlite3": "^12.10.0",
|
"better-sqlite3": "^12.10.0",
|
||||||
|
"gsap": "^3.15.0",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
|
|||||||
Reference in New Issue
Block a user