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 }));
|
||||
}
|
||||
|
||||
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
@@ -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
@@ -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%;
|
||||
|
||||
Generated
+7
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user