loading gradually player sessions instead all at once
This commit is contained in:
@@ -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<SessionEntry[]>([]);
|
||||
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<HTMLDivElement>(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 <div className="text-sm text-ink-faint font-mono">loading…</div>;
|
||||
}
|
||||
if (sessions.length === 0) {
|
||||
return <div className="text-sm text-ink-muted">No recorded sessions yet.</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="row-divide">
|
||||
<div className="grid grid-cols-[1fr_1fr_100px] gap-4 pb-2 text-xs text-ink-faint uppercase tracking-wide">
|
||||
<div>Started</div>
|
||||
<div>Map(s)</div>
|
||||
<div className="text-right">Playtime</div>
|
||||
</div>
|
||||
{sessions.map((s) => (
|
||||
<div key={s.session_id} className="grid grid-cols-[1fr_1fr_100px] gap-4 py-3 text-sm items-center">
|
||||
<div>
|
||||
<div className="text-ink">{new Date(s.session_start_dt.replace(' ', 'T')).toLocaleString()}</div>
|
||||
<div className="text-xs text-ink-faint">
|
||||
{s.session_end_dt
|
||||
? `until ${new Date(s.session_end_dt.replace(' ', 'T')).toLocaleTimeString()}`
|
||||
: 'still connected'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-ink-muted">{s.maps_played ?? '—'}</div>
|
||||
<div className="text-right mono text-ink">{formatMinutes(s.session_active_minutes)}</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div ref={sentinelRef} className="pt-3 text-center">
|
||||
{loadingMore ? (
|
||||
<span className="text-xs text-ink-faint font-mono">loading more…</span>
|
||||
) : !hasMore ? (
|
||||
<span className="text-xs text-ink-faint">— oldest session reached —</span>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user