'use client'; import { useEffect, useRef, useState } from 'react'; import Link from 'next/link'; import { formatMinutes } from '@/lib/steam'; import { useInfiniteScroll } from '@/lib/useInfiniteScroll'; interface MapImpactRow { map_name: string; times_played: number; total_minutes: number; last_played: string; avg_net_change: number | null; avg_retention_pct: number | null; best_session_change: number | null; worst_session_change: number | null; } type SortBy = 'impact' | 'popularity'; type Direction = 'asc' | 'desc'; function ChangeBadge({ value }: { value: number | null }) { if (value == null) return —; const rounded = Math.round(value * 10) / 10; const color = rounded > 0 ? '#3FD37A' : rounded < 0 ? '#E0665C' : '#8A9691'; return ( {rounded > 0 ? '+' : ''} {rounded} ); } export default function MapImpactLeaderboard() { const [sortBy, setSortBy] = useState('impact'); const [direction, setDirection] = useState('asc'); // impact default: worst (most population-harming) first const [maps, setMaps] = useState([]); const [loading, setLoading] = useState(true); const [loadingMore, setLoadingMore] = useState(false); const [hasMore, setHasMore] = useState(true); const offsetRef = useRef(0); const loadingMoreRef = useRef(false); async function fetchPage(sBy: SortBy, dir: Direction, offset: number) { const params = new URLSearchParams({ sortBy: sBy, direction: dir, offset: String(offset) }); const res = await fetch(`/api/maps/impact?${params.toString()}`); return res.json() as Promise<{ maps: MapImpactRow[]; hasMore: boolean }>; } useEffect(() => { setLoading(true); offsetRef.current = 0; fetchPage(sortBy, direction, 0).then((data) => { setMaps(data.maps); setHasMore(data.hasMore); offsetRef.current = data.maps.length; setLoading(false); }); }, [sortBy, direction]); async function loadMore() { if (loadingMoreRef.current || !hasMore || loading) return; loadingMoreRef.current = true; setLoadingMore(true); const data = await fetchPage(sortBy, direction, offsetRef.current); setMaps((prev) => [...prev, ...data.maps]); setHasMore(data.hasMore); offsetRef.current += data.maps.length; setLoadingMore(false); loadingMoreRef.current = false; } const sentinelRef = useInfiniteScroll(loadMore, !loading && hasMore); function switchSort(next: SortBy) { if (next === sortBy) return; setSortBy(next); // Each metric has its own sensible default direction. setDirection(next === 'popularity' ? 'desc' : 'asc'); } return (
Map
Played
Avg Δ
Retention
Best / worst Δ
{loading ? (
loading…
) : maps.length === 0 ? (
No completed map sessions recorded yet.
) : ( maps.map((m, i) => (
{i + 1}
{m.map_name}
{formatMinutes(m.total_minutes)} total
{m.times_played}×
{m.avg_retention_pct == null ? '—' : `${Math.round(m.avg_retention_pct)}%`}
/{' '}
)) )} {!loading && maps.length > 0 && (
{loadingMore ? ( loading more… ) : !hasMore ? ( — end of list — ) : null}
)}
); }