added infinite scrolling to frontend and displaying vote information
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { countryCodeToFlag, formatMinutes } from '@/lib/steam';
|
||||
import { formatRelativeTime } from '@/lib/dates';
|
||||
@@ -20,20 +20,62 @@ export default function PlayerSearch() {
|
||||
const [sort, setSort] = useState<'recent' | 'name' | 'playtime'>('recent');
|
||||
const [players, setPlayers] = useState<Player[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [loadingMore, setLoadingMore] = useState(false);
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
|
||||
const offsetRef = useRef(0);
|
||||
const loadingMoreRef = useRef(false); // guards against the observer firing twice for one fetch
|
||||
const sentinelRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
async function fetchPage(searchQ: string, searchSort: string, offset: number) {
|
||||
const params = new URLSearchParams({ q: searchQ, sort: searchSort, offset: String(offset) });
|
||||
const res = await fetch(`/api/players?${params.toString()}`);
|
||||
return res.json() as Promise<{ players: Player[]; hasMore: boolean }>;
|
||||
}
|
||||
|
||||
// Search or sort changed: reset and load page 1 fresh, replacing the list.
|
||||
useEffect(() => {
|
||||
const handle = setTimeout(async () => {
|
||||
setLoading(true);
|
||||
const params = new URLSearchParams({ q, sort });
|
||||
const res = await fetch(`/api/players?${params.toString()}`);
|
||||
const data = await res.json();
|
||||
offsetRef.current = 0;
|
||||
const data = await fetchPage(q, sort, 0);
|
||||
setPlayers(data.players);
|
||||
setHasMore(data.hasMore);
|
||||
offsetRef.current = data.players.length;
|
||||
setLoading(false);
|
||||
}, 250); // debounce so we're not hitting the DB on every keystroke
|
||||
|
||||
return () => clearTimeout(handle);
|
||||
}, [q, sort]);
|
||||
|
||||
async function loadMore() {
|
||||
if (loadingMoreRef.current || !hasMore || loading) return;
|
||||
loadingMoreRef.current = true;
|
||||
setLoadingMore(true);
|
||||
const data = await fetchPage(q, sort, offsetRef.current);
|
||||
setPlayers((prev) => [...prev, ...data.players]);
|
||||
setHasMore(data.hasMore);
|
||||
offsetRef.current += data.players.length;
|
||||
setLoadingMore(false);
|
||||
loadingMoreRef.current = false;
|
||||
}
|
||||
|
||||
// Fetch the next page once the sentinel at the bottom of the list scrolls
|
||||
// into view, rather than waiting for an explicit "load more" click.
|
||||
useEffect(() => {
|
||||
const el = sentinelRef.current;
|
||||
if (!el) return;
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries[0].isIntersecting) loadMore();
|
||||
},
|
||||
{ rootMargin: '400px' }, // start fetching a bit before it's actually visible
|
||||
);
|
||||
observer.observe(el);
|
||||
return () => observer.disconnect();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [hasMore, loading, q, sort]);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex gap-3">
|
||||
@@ -94,6 +136,17 @@ export default function PlayerSearch() {
|
||||
</Link>
|
||||
))
|
||||
)}
|
||||
|
||||
{/* Sentinel — fetches the next page once this scrolls into view */}
|
||||
{!loading && players.length > 0 && (
|
||||
<div ref={sentinelRef} className="p-4 text-center">
|
||||
{loadingMore ? (
|
||||
<span className="text-xs text-ink-faint font-mono">loading more…</span>
|
||||
) : !hasMore ? (
|
||||
<span className="text-xs text-ink-faint">— end of list —</span>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user