From efe233667febab7649e51fd68a94b593970727c0 Mon Sep 17 00:00:00 2001 From: Heidi Date: Mon, 15 Jun 2026 08:45:24 +0100 Subject: [PATCH] ai generated solutions to our ai generated problems --- backend/src/main.rs | 35 +++++++++++-- frontend/src/App.jsx | 113 +++++++++++++++++++++------------------- frontend/src/styles.css | 18 +------ package-lock.json | 7 +++ package.json | 1 + 5 files changed, 100 insertions(+), 74 deletions(-) diff --git a/backend/src/main.rs b/backend/src/main.rs index b3c35b7..41015af 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -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, ApiError> { +fn leaderboard_teams(state: &AppState, limit: usize) -> Result, 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, _>>() .map_err(db_error)?; + Ok(teams) +} + +fn leaderboard_roster_rows( + state: &AppState, + limit: usize, +) -> Result, 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, 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()) diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 705eaad..e92e312 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -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 (
@@ -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 (
@@ -1533,7 +1538,9 @@ function AppContent() { diff --git a/frontend/src/styles.css b/frontend/src/styles.css index f05de91..bdf99c2 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -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%; diff --git a/package-lock.json b/package-lock.json index 6c8e569..8d49e51 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 2e0a16a..3bfb04f 100644 --- a/package.json +++ b/package.json @@ -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",