From 74e72bfdac850a7879aac3daa2023185ec059349 Mon Sep 17 00:00:00 2001 From: jenz Date: Sat, 12 Sep 2026 13:56:53 +0200 Subject: [PATCH] loading gradually player sessions instead all at once --- .../api/players/[steamid]/sessions/route.ts | 15 +++ .../app/players/[steamid]/page.tsx | 42 ++----- .../components/PlayerSessionsList.tsx | 106 ++++++++++++++++++ .../playtime-frontend/lib/queries.ts | 13 ++- 4 files changed, 141 insertions(+), 35 deletions(-) create mode 100644 discord_verificiation/playtime-frontend/app/api/players/[steamid]/sessions/route.ts create mode 100644 discord_verificiation/playtime-frontend/components/PlayerSessionsList.tsx diff --git a/discord_verificiation/playtime-frontend/app/api/players/[steamid]/sessions/route.ts b/discord_verificiation/playtime-frontend/app/api/players/[steamid]/sessions/route.ts new file mode 100644 index 0000000..a10750b --- /dev/null +++ b/discord_verificiation/playtime-frontend/app/api/players/[steamid]/sessions/route.ts @@ -0,0 +1,15 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getSessions } from '@/lib/queries'; + +const PAGE_SIZE = 10; + +export async function GET(req: NextRequest, { params }: { params: Promise<{ steamid: string }> }) { + const { steamid: rawSteamid } = await params; + const steamid = decodeURIComponent(rawSteamid); + const offset = Math.max(0, Number(req.nextUrl.searchParams.get('offset')) || 0); + + const sessions = await getSessions(steamid, PAGE_SIZE, offset); + const hasMore = sessions.length === PAGE_SIZE; + + return NextResponse.json({ sessions, hasMore }); +} diff --git a/discord_verificiation/playtime-frontend/app/players/[steamid]/page.tsx b/discord_verificiation/playtime-frontend/app/players/[steamid]/page.tsx index 2c95ca1..5159722 100644 --- a/discord_verificiation/playtime-frontend/app/players/[steamid]/page.tsx +++ b/discord_verificiation/playtime-frontend/app/players/[steamid]/page.tsx @@ -1,9 +1,10 @@ import { notFound } from 'next/navigation'; import Link from 'next/link'; -import { getPlayer, getNameHistory, getSessions, getTotalPlaytimeMinutes } from '@/lib/queries'; +import { getPlayer, getNameHistory, getTotalPlaytimeMinutes } from '@/lib/queries'; import { steam2ToSteam64, steamProfileUrl, formatMinutes, countryCodeToFlag } from '@/lib/steam'; import { getSteamSummary, DEFAULT_AVATAR_URL } from '@/lib/steamApi'; import { getRaceTimerSummary } from '@/lib/racetimer'; +import PlayerSessionsList from '@/components/PlayerSessionsList'; export default async function PlayerDetailPage({ params, @@ -21,9 +22,14 @@ export default async function PlayerDetailPage({ const steamId64 = steam2ToSteam64(steamid); const profileUrl = steamProfileUrl(steamid); - const [nameHistory, sessions, totalMinutes, steamSummary, raceTimer] = await Promise.all([ + // Sessions are deliberately NOT fetched here — that query's per-row + // maps_played subquery gets slower as a player accumulates history, and + // was making this page take 20-30+ seconds for long-tenured players + // regardless of whether anyone scrolled far enough to see old sessions. + // PlayerSessionsList below fetches its own first page client-side, so + // everything else on this page renders immediately. + const [nameHistory, totalMinutes, steamSummary, raceTimer] = await Promise.all([ getNameHistory(steamid), - getSessions(steamid), getTotalPlaytimeMinutes(steamid), steamId64 ? getSteamSummary(steamId64) : Promise.resolve(null), getRaceTimerSummary(steamid), @@ -120,35 +126,7 @@ export default async function PlayerDetailPage({ {/* Sessions */}
Play sessions
- {sessions.length === 0 ? ( -
No recorded sessions yet.
- ) : ( -
-
-
Started
-
Map(s)
-
Playtime
-
- {sessions.map((s) => ( -
-
-
- {new Date(s.session_start_dt.replace(' ', 'T')).toLocaleString()} -
-
- {s.session_end_dt - ? `until ${new Date(s.session_end_dt.replace(' ', 'T')).toLocaleTimeString()}` - : 'still connected'} -
-
-
{s.maps_played ?? '—'}
-
- {formatMinutes(s.session_active_minutes)} -
-
- ))} -
- )} +
); diff --git a/discord_verificiation/playtime-frontend/components/PlayerSessionsList.tsx b/discord_verificiation/playtime-frontend/components/PlayerSessionsList.tsx new file mode 100644 index 0000000..6a2306c --- /dev/null +++ b/discord_verificiation/playtime-frontend/components/PlayerSessionsList.tsx @@ -0,0 +1,106 @@ +'use client'; + +import { useEffect, useRef, useState } from 'react'; +import { formatMinutes } from '@/lib/steam'; + +interface SessionEntry { + session_id: number; + session_start_dt: string; + session_end_dt: string | null; + session_active_minutes: number | null; + maps_played: string | null; +} + +export default function PlayerSessionsList({ steamid }: { steamid: string }) { + const [sessions, setSessions] = useState([]); + const [loading, setLoading] = useState(true); + const [loadingMore, setLoadingMore] = useState(false); + const [hasMore, setHasMore] = useState(true); + + const offsetRef = useRef(0); + const loadingMoreRef = useRef(false); + const sentinelRef = useRef(null); + + async function fetchPage(offset: number) { + const res = await fetch(`/api/players/${encodeURIComponent(steamid)}/sessions?offset=${offset}`); + return res.json() as Promise<{ sessions: SessionEntry[]; hasMore: boolean }>; + } + + useEffect(() => { + (async () => { + setLoading(true); + offsetRef.current = 0; + const data = await fetchPage(0); + setSessions(data.sessions); + setHasMore(data.hasMore); + offsetRef.current = data.sessions.length; + setLoading(false); + })(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [steamid]); + + async function loadMore() { + if (loadingMoreRef.current || !hasMore || loading) return; + loadingMoreRef.current = true; + setLoadingMore(true); + const data = await fetchPage(offsetRef.current); + setSessions((prev) => [...prev, ...data.sessions]); + setHasMore(data.hasMore); + offsetRef.current += data.sessions.length; + setLoadingMore(false); + loadingMoreRef.current = false; + } + + useEffect(() => { + const el = sentinelRef.current; + if (!el) return; + const observer = new IntersectionObserver( + (entries) => { + if (entries[0].isIntersecting) loadMore(); + }, + { rootMargin: '400px' }, + ); + observer.observe(el); + return () => observer.disconnect(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [hasMore, loading, steamid]); + + if (loading) { + return
loading…
; + } + if (sessions.length === 0) { + return
No recorded sessions yet.
; + } + + return ( +
+
+
Started
+
Map(s)
+
Playtime
+
+ {sessions.map((s) => ( +
+
+
{new Date(s.session_start_dt.replace(' ', 'T')).toLocaleString()}
+
+ {s.session_end_dt + ? `until ${new Date(s.session_end_dt.replace(' ', 'T')).toLocaleTimeString()}` + : 'still connected'} +
+
+
{s.maps_played ?? '—'}
+
{formatMinutes(s.session_active_minutes)}
+
+ ))} + +
+ {loadingMore ? ( + loading more… + ) : !hasMore ? ( + — oldest session reached — + ) : null} +
+
+ ); +} diff --git a/discord_verificiation/playtime-frontend/lib/queries.ts b/discord_verificiation/playtime-frontend/lib/queries.ts index a520836..7658db4 100644 --- a/discord_verificiation/playtime-frontend/lib/queries.ts +++ b/discord_verificiation/playtime-frontend/lib/queries.ts @@ -62,11 +62,18 @@ export async function getNameHistory(steamid: string): Promise { +export async function getSessions(steamid: string, limit = 10, offset = 0): Promise { // The maps_played subquery finds every map_history period that overlaps // this session's [start, end] window (same overlap logic used elsewhere // in the app), and concatenates the names. A session can span more than // one map if the player stayed connected through a map change. + // + // This subquery runs once per returned row, and gets slower as + // map_history grows — which is exactly why this is paginated at a small + // page size (10) rather than fetching a large batch at once. A profile + // with thousands of historical sessions was taking 20-30+ seconds to + // load because this ran against a much larger LIMIT on every visit, + // regardless of whether anyone ever scrolled down to see older sessions. return query( `SELECT s.session_id, @@ -83,8 +90,8 @@ export async function getSessions(steamid: string, limit = 100): Promise