ai generated solutions to our ai generated problems

This commit is contained in:
Heidi
2026-06-15 08:45:24 +01:00
parent c94a09f46c
commit efe233667f
5 changed files with 100 additions and 74 deletions
+30 -5
View File
@@ -386,14 +386,12 @@ async fn leaderboard(
return Ok(Json(LeaderboardResponse { teams }));
}
let rows = leaderboard_rows(&state, limit)?;
cache_leaderboard(&state, &rows)?;
Ok(Json(LeaderboardResponse { teams: rows }))
let teams = leaderboard_roster_rows(&state, limit)?;
Ok(Json(LeaderboardResponse { teams }))
}
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 battles_conn = open_db(&state.battles_db)?;
// 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.
let mut stmt = teams_conn
@@ -411,6 +409,33 @@ fn leaderboard_rows(state: &AppState, limit: usize) -> Result<Vec<TeamLeaderboar
.map_err(db_error)?
.collect::<Result<Vec<_>, _>>()
.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
.iter()
.map(|team| team.name.as_str())
+60 -53
View File
@@ -1,9 +1,13 @@
import { useEffect, useMemo, useRef, useState } from 'react'
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
import L from 'leaflet'
import { gsap } from 'gsap'
import { ScrollTrigger } from 'gsap/ScrollTrigger'
import 'leaflet/dist/leaflet.css'
import Tree, { prewarmTreeCanvas } from '../Tree/Tree'
import FallingLeaves from '../Tree/FallingLeaves'
gsap.registerPlugin(ScrollTrigger)
const numberFormat = new Intl.NumberFormat('en-GB')
const dateFormat = new Intl.DateTimeFormat('en-GB', {
dateStyle: 'medium',
@@ -593,53 +597,51 @@ function themeToggleDockPosition(navPill) {
}
}
function ThemeToggleMover({ position, theme, onThemeChange }) {
const previousPositionRef = useRef(position)
const [motion, setMotion] = useState(() => ({
from: position,
to: position,
mid: position,
key: 0,
animate: false,
}))
function ThemeToggleMover({ dockPosition, homePosition, isHome, theme, onThemeChange }) {
const moverRef = useRef(null)
useEffect(() => {
const from = previousPositionRef.current
const to = position
if (from.x === to.x && from.y === to.y) return
useLayoutEffect(() => {
const mover = moverRef.current
if (!mover) return undefined
const distance = Math.hypot(to.x - from.x, to.y - from.y)
const lift = Math.min(96, Math.max(24, distance * 0.16))
const mid = {
x: Math.round((from.x + to.x) / 2),
y: Math.round(Math.min(from.y, to.y) - lift),
if (!isHome) {
gsap.to(mover, {
x: dockPosition.x,
y: dockPosition.y,
duration: 0.56,
ease: 'power3.out',
overwrite: true,
})
return undefined
}
previousPositionRef.current = to
setMotion((current) => ({
from,
to,
mid,
key: current.key + 1,
animate: true,
}))
}, [position])
const tween = gsap.fromTo(
mover,
{ x: homePosition.x, y: homePosition.y },
{
x: dockPosition.x,
y: dockPosition.y,
ease: 'none',
overwrite: true,
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 (
<div
className={`theme-toggle-mover fixed left-0 top-0 z-[60] ${
motion.animate ? 'theme-toggle-mover-arc' : ''
}`}
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)`,
}}
className="theme-toggle-mover fixed left-0 top-0 z-[60]"
ref={moverRef}
>
<ThemeToggle theme={theme} onThemeChange={onThemeChange} />
</div>
@@ -821,7 +823,10 @@ function AppContent() {
const [analyticsPreferences, setAnalyticsPreferences] = useState(() => storedAnalyticsPreferences())
const [theme, setTheme] = useState(() => storedThemePreference())
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 [searchHint, setSearchHint] = useState({ status: 'idle', name: '' })
const [teamSearchResults, setTeamSearchResults] = useState([])
@@ -1476,24 +1481,24 @@ function AppContent() {
useEffect(() => {
let frame = 0
function updateThemeTogglePosition() {
function updateThemeTogglePositions() {
window.cancelAnimationFrame(frame)
frame = window.requestAnimationFrame(() => {
setThemeTogglePosition(
shouldShowFloatingNav
? themeToggleDockPosition(navPillRef.current)
: defaultThemeTogglePosition(),
)
setThemeTogglePositions({
dock: themeToggleDockPosition(navPillRef.current),
home: defaultThemeTogglePosition(),
})
ScrollTrigger.refresh()
})
}
updateThemeTogglePosition()
window.addEventListener('resize', updateThemeTogglePosition)
updateThemeTogglePositions()
window.addEventListener('resize', updateThemeTogglePositions)
return () => {
window.cancelAnimationFrame(frame)
window.removeEventListener('resize', updateThemeTogglePosition)
window.removeEventListener('resize', updateThemeTogglePositions)
}
}, [route.page, shouldShowFloatingNav])
}, [route.page])
return (
<main className="min-h-screen bg-bg text-text">
@@ -1533,7 +1538,9 @@ function AppContent() {
</div>
</header>
<ThemeToggleMover
position={themeTogglePosition}
dockPosition={themeTogglePositions.dock}
homePosition={themeTogglePositions.home}
isHome={route.page === 'home'}
theme={theme}
onThemeChange={chooseTheme}
/>
+2 -16
View File
@@ -477,8 +477,8 @@ h3 {
0 1px 3px rgba(0, 0, 0, 0.35);
}
.theme-toggle-mover-arc {
animation: themeToggleArc 560ms cubic-bezier(0.22, 1, 0.36, 1) forwards;
.theme-toggle-mover {
will-change: transform;
}
: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 {
0% {
offset-distance: 0%;
+7
View File
@@ -11,6 +11,7 @@
"@tailwindcss/vite": "^4.1.8",
"@vitejs/plugin-react": "^4.5.0",
"better-sqlite3": "^12.10.0",
"gsap": "^3.15.0",
"leaflet": "^1.9.4",
"react": "^19.1.0",
"react-dom": "^19.1.0",
@@ -3130,6 +3131,12 @@
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"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": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+1
View File
@@ -19,6 +19,7 @@
"@tailwindcss/vite": "^4.1.8",
"@vitejs/plugin-react": "^4.5.0",
"better-sqlite3": "^12.10.0",
"gsap": "^3.15.0",
"leaflet": "^1.9.4",
"react": "^19.1.0",
"react-dom": "^19.1.0",