'use client'; import { useEffect, useRef, useState } from 'react'; import Link from 'next/link'; import { countryCodeToFlag, formatMinutes } from '@/lib/steam'; import { formatRelativeTime } from '@/lib/dates'; interface Player { steamid: string; current_name: string; current_country: string | null; matched_name: string | null; total_minutes: number | null; last_connected: string | null; avatarUrl: string; } export default function PlayerSearch() { const [q, setQ] = useState(''); const [sort, setSort] = useState<'recent' | 'name' | 'playtime'>('recent'); const [players, setPlayers] = useState([]); 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(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); 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 (
setQ(e.target.value)} className="flex-1 bg-base-panel border border-base-border rounded-lg px-4 py-3 text-ink placeholder:text-ink-faint focus:border-accent/50 transition-colors" />
{loading ? (
loading…
) : players.length === 0 ? (
No players found.
) : ( players.map((p, i) => (
{i + 1} {/* eslint-disable-next-line @next/next/no-img-element */} {countryCodeToFlag(p.current_country)}
{p.current_name}
{p.matched_name && p.matched_name !== p.current_name && (
previously: {p.matched_name}
)}
{formatRelativeTime(p.last_connected)} {formatMinutes(p.total_minutes)} {p.steamid}
)) )} {/* Sentinel — fetches the next page once this scrolls into view */} {!loading && players.length > 0 && (
{loadingMore ? ( loading more… ) : !hasMore ? ( — end of list — ) : null}
)}
); }