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