diff --git a/app/game/[id]/game-page-client.tsx b/app/game/[id]/game-page-client.tsx index b28c8f9..8287c93 100644 --- a/app/game/[id]/game-page-client.tsx +++ b/app/game/[id]/game-page-client.tsx @@ -1,6 +1,6 @@ "use client" -import { useCallback, useState, useEffect, useMemo } from "react" +import { useCallback, useState, useEffect, useMemo, useRef } from "react" import Image from "next/image" import { Gamepad2Icon, @@ -215,11 +215,14 @@ export function GamePageClient({ }, [gameId]) // Initialize selected devices when stats load + const didInitDevices = useRef(false) useEffect(() => { - if (stats && selectedDevices.length === 0) { - setSelectedDevices(stats.deviceBreakdown.map((d) => d.hardwareSlug)) + if (stats && !didInitDevices.current) { + didInitDevices.current = true + const slugs = stats.deviceBreakdown.map((d) => d.hardwareSlug) + queueMicrotask(() => setSelectedDevices(slugs)) } - }, [stats, selectedDevices.length]) + }, [stats]) // Filtered stats const filteredStats = useMemo(() => { diff --git a/components/charts/FpsBoxplot.tsx b/components/charts/FpsBoxplot.tsx index 580f67c..0c220b1 100644 --- a/components/charts/FpsBoxplot.tsx +++ b/components/charts/FpsBoxplot.tsx @@ -1,7 +1,7 @@ "use client" import { useMemo } from "react" -import { EChartWrapper, CHART_THEME, getDeviceColor } from "./EChartWrapper" +import { EChartWrapper, CHART_THEME } from "./EChartWrapper" import type { EChartsOption } from "echarts" interface BoxplotEntry { diff --git a/components/navbar.tsx b/components/navbar.tsx index 1c69266..1611e58 100644 --- a/components/navbar.tsx +++ b/components/navbar.tsx @@ -4,7 +4,7 @@ import { AnimatePresence, motion } from "motion/react" import logo from "@/app/icon.png" import Image from "next/image" import Link from "next/link" -import { useEffect, useLayoutEffect, useRef, useState } from "react" +import { useEffect, useRef, useState } from "react" import { CircleXIcon, Gamepad2Icon, MenuIcon, XIcon } from "lucide-react" import { routes } from "@/lib/routes" import { usePathname, useRouter, useSearchParams } from "next/navigation" @@ -37,14 +37,19 @@ export default function Navbar() { const [userMenuOpen, setUserMenuOpen] = useState(false) // Sync search query with URL ?q= param - useLayoutEffect(() => { + const searchQueryRef = useRef(searchQuery) + useEffect(() => { + searchQueryRef.current = searchQuery + }, [searchQuery]) + + useEffect(() => { const q = searchParams.get("q") || "" + if (q === searchQueryRef.current) return // If the URL has a different value than our state, we're syncing after // a navigation — skip the next URL-write to avoid clearing the param - if (q !== searchQuery) { - skipNextUrlWrite.current = true - } - setSearchQuery(q) + skipNextUrlWrite.current = true + const id = setTimeout(() => setSearchQuery(q), 0) + return () => clearTimeout(id) }, [searchParams]) // Update URL when debounced query changes (skip if already matches) diff --git a/lib/api/search-unified.ts b/lib/api/search-unified.ts index fafc4b3..78aa2a5 100644 --- a/lib/api/search-unified.ts +++ b/lib/api/search-unified.ts @@ -147,8 +147,8 @@ export const searchUnifiedRoutes = new Elysia({ prefix: "/search" }).get( } // ── 2b. Raw Performer + best FPS ──────────────────────────────── - let rawPerformerMap = new Map() - let bestFpsMap = new Map() + const rawPerformerMap = new Map() + const bestFpsMap = new Map() if (localGameIds.length > 0) { const perfStats = await db @@ -181,7 +181,7 @@ export const searchUnifiedRoutes = new Elysia({ prefix: "/search" }).get( } // ── 2c. Latest version ────────────────────────────────────────── - let latestVersionMap = new Map() + const latestVersionMap = new Map() if (localGameIds.length > 0) { const versionRows = await db