import { useState, useMemo, useCallback, memo, useRef, useEffect } from "react"; import { Text, TextInput, Stack, Group, Box, ThemeIcon, Container, Title, Divider, UnstyledButton, Popover, ActionIcon, ScrollArea, } from "@mantine/core"; import { MagnifyingGlassIcon, CaretUpIcon, CaretDownIcon, ChartBarIcon, InfoIcon, } from "@phosphor-icons/react"; import { PlayerStats } from "../types"; import Avatar from "@/components/avatar"; import { useNavigate } from "@tanstack/react-router"; import { useAllPlayerStats } from "../queries"; type SortKey = keyof PlayerStats | "mmr"; type SortDirection = "asc" | "desc"; interface SortConfig { key: SortKey; direction: SortDirection; } interface PlayerListItemProps { stat: PlayerStats; onPlayerClick: (playerId: string) => void; mmr: number; onRegisterViewport: (viewport: HTMLDivElement) => void; onUnregisterViewport: (viewport: HTMLDivElement) => void; } interface StatCellProps { label: string; value: string | number; } const StatCell = memo(({ label, value }: StatCellProps) => ( {label} {value} )); const PlayerListItem = memo(({ stat, onPlayerClick, mmr, onRegisterViewport, onUnregisterViewport }: PlayerListItemProps) => { const viewportRef = useRef(null); const avg_cups_against = useMemo(() => stat.total_cups_against / stat.matches || 0, [stat.total_cups_against, stat.matches]); useEffect(() => { if (viewportRef.current) { onRegisterViewport(viewportRef.current); return () => { if (viewportRef.current) { onUnregisterViewport(viewportRef.current); } }; } }, [onRegisterViewport, onUnregisterViewport]); return ( <> onPlayerClick(stat.id)} style={{ borderRadius: 0, transition: "background-color 0.15s ease", }} styles={{ root: { '&:hover': { backgroundColor: 'var(--mantine-color-gray-0)', }, }, }} > {stat.player_name} {stat.matches} M {stat.tournaments} T ); }); const PlayerStatsTable = () => { const { data: playerStats } = useAllPlayerStats(); const navigate = useNavigate(); const [search, setSearch] = useState(""); const [sortConfig, setSortConfig] = useState({ key: "mmr" as SortKey, direction: "desc", }); const viewportsRef = useRef>(new Set()); const scrollHandlersRef = useRef void>>(new Map()); const scrollLeaderRef = useRef(null); const scrollTimeoutRef = useRef(null); const handleRegisterViewport = useCallback((viewport: HTMLDivElement) => { viewportsRef.current.add(viewport); const handleScrollStart = () => { scrollLeaderRef.current = viewport; }; const handleScroll = (e: Event) => { const target = e.target as HTMLDivElement; if (!scrollLeaderRef.current) { scrollLeaderRef.current = target; } if (scrollLeaderRef.current !== target) { return; } const scrollLeft = target.scrollLeft; viewportsRef.current.forEach((vp) => { if (vp !== target && Math.abs(vp.scrollLeft - scrollLeft) > 0.5) { vp.scrollLeft = scrollLeft; } }); if (scrollTimeoutRef.current) { clearTimeout(scrollTimeoutRef.current); } scrollTimeoutRef.current = window.setTimeout(() => { scrollLeaderRef.current = null; }, 150); }; viewport.addEventListener('touchstart', handleScrollStart, { passive: true }); viewport.addEventListener('mousedown', handleScrollStart, { passive: true }); viewport.addEventListener('scroll', handleScroll, { passive: true }); scrollHandlersRef.current.set(viewport, handleScroll); }, []); const handleUnregisterViewport = useCallback((viewport: HTMLDivElement) => { viewportsRef.current.delete(viewport); const handler = scrollHandlersRef.current.get(viewport); if (handler) { viewport.removeEventListener('scroll', handler); viewport.removeEventListener('touchstart', handler); viewport.removeEventListener('mousedown', handler); scrollHandlersRef.current.delete(viewport); } }, []); const calculateMMR = (stat: PlayerStats): number => { if (stat.matches === 0) return 0; const winScore = stat.win_percentage; const matchConfidence = Math.min(stat.matches / 15, 1); const avgCupsScore = Math.min(stat.avg_cups_per_match * 10, 100); const marginScore = stat.margin_of_victory ? Math.min(stat.margin_of_victory * 20, 50) : 0; const volumeBonus = Math.min(stat.matches * 0.5, 10); const baseMMR = winScore * 0.5 + avgCupsScore * 0.25 + marginScore * 0.15 + volumeBonus * 0.1; const finalMMR = baseMMR * matchConfidence; return Math.round(finalMMR * 10) / 10; }; const statsWithMMR = useMemo(() => { return playerStats.map((stat) => ({ ...stat, mmr: calculateMMR(stat), })); }, [playerStats]); const filteredAndSortedStats = useMemo(() => { let filtered = statsWithMMR.filter((stat) => stat.player_name.toLowerCase().includes(search.toLowerCase()) ); return filtered.sort((a, b) => { let aValue: number | string; let bValue: number | string; if (sortConfig.key === "mmr") { aValue = a.mmr; bValue = b.mmr; } else { aValue = a[sortConfig.key]; bValue = b[sortConfig.key]; } if (typeof aValue === "number" && typeof bValue === "number") { return sortConfig.direction === "desc" ? bValue - aValue : aValue - bValue; } if (typeof aValue === "string" && typeof bValue === "string") { return sortConfig.direction === "desc" ? bValue.localeCompare(aValue) : aValue.localeCompare(bValue); } return 0; }); }, [statsWithMMR, search, sortConfig]); const handlePlayerClick = useCallback((playerId: string) => { navigate({ to: `/profile/${playerId}` }); }, [navigate]); const handleSort = (key: SortKey) => { setSortConfig((prev) => ({ key, direction: prev.key === key && prev.direction === "desc" ? "asc" : "desc", })); }; const getSortIcon = (key: SortKey) => { if (sortConfig.key !== key) return null; return sortConfig.direction === "desc" ? ( ) : ( ); }; if (playerStats.length === 0) { return ( No Stats Available ); } return ( Showing {filteredAndSortedStats.length} of {playerStats.length} players setSearch(e.currentTarget.value)} leftSection={} size="md" px="md" />
Sort: handleSort("mmr")} style={{ display: "flex", alignItems: "center", gap: 4 }} > MMR {getSortIcon("mmr")} handleSort("wins")} style={{ display: "flex", alignItems: "center", gap: 4 }} > Wins {getSortIcon("wins")} handleSort("matches")} style={{ display: "flex", alignItems: "center", gap: 4 }} > Matches {getSortIcon("matches")} Stat Abbreviations: M: Matches T: Tournaments MMR: Matchmaking Rating W: Wins L: Losses W%: Win Percentage AWM: Average Win Margin ALM: Average Loss Margin AC: Average Cups Per Match ACA: Average Cups Against CF: Cups For CA: Cups Against MMR Calculation: • Win Rate (50%) • Average Cups/Match (25%) • Average Win Margin (15%) • Match Volume Bonus (10%) * Confidence penalty applied for players with <15 matches ** Not an official rating
{filteredAndSortedStats.map((stat, index) => ( {index < filteredAndSortedStats.length - 1 && } ))} {filteredAndSortedStats.length === 0 && search && ( No players found matching "{search}" )}
); }; export default PlayerStatsTable;