added infinite scrolling to frontend and displaying vote information

This commit is contained in:
jenz
2026-08-25 21:34:45 +02:00
parent ab481d6d65
commit 0634fde335
7 changed files with 240 additions and 25 deletions
@@ -1,6 +1,6 @@
'use client';
import { useEffect, useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import Link from 'next/link';
import { formatMinutes } from '@/lib/steam';
@@ -15,19 +15,59 @@ export default function MapSearch() {
const [q, setQ] = useState('');
const [maps, setMaps] = useState<MapSummary[]>([]);
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(searchQ: string, offset: number) {
const params = new URLSearchParams({ q: searchQ, offset: String(offset) });
const res = await fetch(`/api/maps?${params.toString()}`);
return res.json() as Promise<{ maps: MapSummary[]; hasMore: boolean }>;
}
useEffect(() => {
const handle = setTimeout(async () => {
setLoading(true);
const res = await fetch(`/api/maps?q=${encodeURIComponent(q)}`);
const data = await res.json();
offsetRef.current = 0;
const data = await fetchPage(q, 0);
setMaps(data.maps);
setHasMore(data.hasMore);
offsetRef.current = data.maps.length;
setLoading(false);
}, 250);
return () => clearTimeout(handle);
}, [q]);
async function loadMore() {
if (loadingMoreRef.current || !hasMore || loading) return;
loadingMoreRef.current = true;
setLoadingMore(true);
const data = await fetchPage(q, offsetRef.current);
setMaps((prev) => [...prev, ...data.maps]);
setHasMore(data.hasMore);
offsetRef.current += data.maps.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, q]);
return (
<div className="space-y-4">
<input
@@ -71,6 +111,16 @@ export default function MapSearch() {
</Link>
))
)}
{!loading && maps.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>
);