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 })); 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
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 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
View File
@@ -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%;
+7
View File
@@ -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",
+1
View File
@@ -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",