diff --git a/src/features/players/components/player-stats-table-skeleton.tsx b/src/features/players/components/player-stats-table-skeleton.tsx
index 013c768..b341608 100644
--- a/src/features/players/components/player-stats-table-skeleton.tsx
+++ b/src/features/players/components/player-stats-table-skeleton.tsx
@@ -5,52 +5,66 @@ import {
Container,
Divider,
Skeleton,
+ ScrollArea,
} from "@mantine/core";
const PlayerListItemSkeleton = () => {
return (
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
+
-
+
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
-
+
+
);
@@ -60,13 +74,13 @@ const PlayerStatsTableSkeleton = () => {
return (
-
+
+
-
-
-
+
+
diff --git a/src/features/players/components/player-stats-table.tsx b/src/features/players/components/player-stats-table.tsx
index d1b48d6..8f47063 100644
--- a/src/features/players/components/player-stats-table.tsx
+++ b/src/features/players/components/player-stats-table.tsx
@@ -1,4 +1,4 @@
-import { useState, useMemo, useCallback, memo, use } from "react";
+import { useState, useMemo, useCallback, memo, useRef, useEffect } from "react";
import {
Text,
TextInput,
@@ -12,6 +12,7 @@ import {
UnstyledButton,
Popover,
ActionIcon,
+ ScrollArea,
} from "@mantine/core";
import {
MagnifyingGlassIcon,
@@ -37,9 +38,41 @@ interface PlayerListItemProps {
stat: PlayerStats;
onPlayerClick: (playerId: string) => void;
mmr: number;
+ onRegisterViewport: (viewport: HTMLDivElement) => void;
+ onUnregisterViewport: (viewport: HTMLDivElement) => void;
}
-const PlayerListItem = memo(({ stat, onPlayerClick, mmr }: PlayerListItemProps) => {
+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 (
<>
@@ -59,92 +92,41 @@ const PlayerListItem = memo(({ stat, onPlayerClick, mmr }: PlayerListItemProps)
},
}}
>
-
-
-
-
-
-
- {stat.player_name}
-
-
- {stat.matches}
- M
-
-
- {stat.tournaments}
- T
-
+
+
+
+
+
+ {stat.player_name}
+
+
+ {stat.matches}
+ M
+
+
+ {stat.tournaments}
+ T
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
- MMR
-
-
- {mmr.toFixed(1)}
-
-
-
-
- W
-
-
- {stat.wins}
-
-
-
-
- L
-
-
- {stat.losses}
-
-
-
-
- W%
-
-
- {stat.win_percentage.toFixed(1)}%
-
-
-
-
- AWM
-
-
- {stat.margin_of_victory?.toFixed(1) || 0}
-
-
-
-
- AC
-
-
- {stat.avg_cups_per_match.toFixed(1)}
-
-
-
-
- CF
-
-
- {stat.total_cups_made}
-
-
-
-
- CA
-
-
- {stat.total_cups_against}
-
-
-
-
-
-
+
+
>
@@ -160,6 +142,37 @@ const PlayerStatsTable = () => {
direction: "desc",
});
+ const viewportsRef = useRef>(new Set());
+ const isScrollingRef = useRef(false);
+
+ const handleRegisterViewport = useCallback((viewport: HTMLDivElement) => {
+ viewportsRef.current.add(viewport);
+
+ const handleScroll = (e: Event) => {
+ if (isScrollingRef.current) return;
+
+ isScrollingRef.current = true;
+ const scrollLeft = (e.target as HTMLDivElement).scrollLeft;
+
+ viewportsRef.current.forEach((vp) => {
+ if (vp !== e.target) {
+ vp.scrollLeft = scrollLeft;
+ }
+ });
+
+ requestAnimationFrame(() => {
+ isScrollingRef.current = false;
+ });
+ };
+
+ viewport.addEventListener('scroll', handleScroll);
+ viewport.dataset.scrollHandler = 'attached';
+ }, []);
+
+ const handleUnregisterViewport = useCallback((viewport: HTMLDivElement) => {
+ viewportsRef.current.delete(viewport);
+ }, []);
+
const calculateMMR = (stat: PlayerStats): number => {
if (stat.matches === 0) return 0;
@@ -259,6 +272,9 @@ const PlayerStatsTable = () => {
return (
+
+ Showing {filteredAndSortedStats.length} of {playerStats.length} players
+
{
/>
-
- {filteredAndSortedStats.length} of {playerStats.length} players
-
-
- Sort:
+
+
+ Sort:
handleSort("mmr")}
style={{ display: "flex", alignItems: "center", gap: 4 }}
@@ -335,9 +349,15 @@ const PlayerStatsTable = () => {
• AWM: Average Win Margin
+
+ • ALM: Average Loss Margin
+
• AC: Average Cups Per Match
+
+ • ACA: Average Cups Against
+
• CF: Cups For
@@ -381,6 +401,8 @@ const PlayerStatsTable = () => {
stat={stat}
onPlayerClick={handlePlayerClick}
mmr={stat.mmr}
+ onRegisterViewport={handleRegisterViewport}
+ onUnregisterViewport={handleUnregisterViewport}
/>
{index < filteredAndSortedStats.length - 1 && }