more stats
This commit is contained in:
@@ -5,52 +5,66 @@ import {
|
||||
Container,
|
||||
Divider,
|
||||
Skeleton,
|
||||
ScrollArea,
|
||||
} from "@mantine/core";
|
||||
|
||||
const PlayerListItemSkeleton = () => {
|
||||
return (
|
||||
<Box p="md">
|
||||
<Group justify="space-between" align="center" w="100%">
|
||||
<Group gap="sm" align="center">
|
||||
<Skeleton height={45} circle />
|
||||
<Stack gap={2}>
|
||||
<Group gap='xs'>
|
||||
<Skeleton height={16} width={120} />
|
||||
<Skeleton height={12} width={60} />
|
||||
<Skeleton height={12} width={80} />
|
||||
</Group>
|
||||
<Group gap="md" ta="center">
|
||||
<Stack gap={0}>
|
||||
<Group gap="sm" align="center" w="100%" wrap="nowrap" style={{ overflow: 'hidden' }}>
|
||||
<Skeleton height={40} width={40} circle style={{ flexShrink: 0 }} />
|
||||
<Stack gap={2} style={{ flexGrow: 1, overflow: 'hidden', minWidth: 0 }}>
|
||||
<Group gap='xs'>
|
||||
<Skeleton height={16} width={120} />
|
||||
<Skeleton height={12} width={30} />
|
||||
<Skeleton height={12} width={30} />
|
||||
</Group>
|
||||
|
||||
<ScrollArea type="never">
|
||||
<Group gap='xs' wrap="nowrap">
|
||||
<Stack gap={0} style={{ flexShrink: 0 }}>
|
||||
<Skeleton height={10} width={25} />
|
||||
<Skeleton height={10} width={30} />
|
||||
</Stack>
|
||||
<Stack gap={0}>
|
||||
<Stack gap={0} style={{ flexShrink: 0 }}>
|
||||
<Skeleton height={10} width={10} />
|
||||
<Skeleton height={10} width={15} />
|
||||
</Stack>
|
||||
<Stack gap={0}>
|
||||
<Stack gap={0} style={{ flexShrink: 0 }}>
|
||||
<Skeleton height={10} width={10} />
|
||||
<Skeleton height={10} width={15} />
|
||||
</Stack>
|
||||
<Stack gap={0}>
|
||||
<Stack gap={0} style={{ flexShrink: 0 }}>
|
||||
<Skeleton height={10} width={20} />
|
||||
<Skeleton height={10} width={25} />
|
||||
</Stack>
|
||||
<Stack gap={0}>
|
||||
<Stack gap={0} style={{ flexShrink: 0 }}>
|
||||
<Skeleton height={10} width={25} />
|
||||
<Skeleton height={10} width={20} />
|
||||
</Stack>
|
||||
<Stack gap={0}>
|
||||
<Stack gap={0} style={{ flexShrink: 0 }}>
|
||||
<Skeleton height={10} width={25} />
|
||||
<Skeleton height={10} width={20} />
|
||||
</Stack>
|
||||
<Stack gap={0} style={{ flexShrink: 0 }}>
|
||||
<Skeleton height={10} width={30} />
|
||||
<Skeleton height={10} width={25} />
|
||||
</Stack>
|
||||
<Stack gap={0} style={{ flexShrink: 0 }}>
|
||||
<Skeleton height={10} width={30} />
|
||||
<Skeleton height={10} width={25} />
|
||||
</Stack>
|
||||
<Stack gap={0} style={{ flexShrink: 0 }}>
|
||||
<Skeleton height={10} width={15} />
|
||||
<Skeleton height={10} width={25} />
|
||||
</Stack>
|
||||
<Stack gap={0}>
|
||||
<Stack gap={0} style={{ flexShrink: 0 }}>
|
||||
<Skeleton height={10} width={15} />
|
||||
<Skeleton height={10} width={25} />
|
||||
</Stack>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Group>
|
||||
</ScrollArea>
|
||||
</Stack>
|
||||
</Group>
|
||||
</Box>
|
||||
);
|
||||
@@ -60,13 +74,13 @@ const PlayerStatsTableSkeleton = () => {
|
||||
return (
|
||||
<Container size="100%" px={0}>
|
||||
<Stack gap="xs">
|
||||
<Box px="md" pb="xs">
|
||||
<Skeleton mx="md" height={12} width={100} />
|
||||
<Box px="md" pb={4}>
|
||||
<Skeleton height={40} />
|
||||
</Box>
|
||||
|
||||
<Group px="md" justify="space-between" align="center">
|
||||
<Skeleton height={12} width={100} />
|
||||
<Group gap="xs">
|
||||
<Group justify="space-between" align="center" w='100%'>
|
||||
<Group ml="auto" gap="xs">
|
||||
<Skeleton height={12} width={200} />
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
@@ -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) => (
|
||||
<Stack justify="center" gap={0} style={{ textAlign: 'center', flexShrink: 0 }}>
|
||||
<Text size="xs" c="dimmed" fw={700}>
|
||||
{label}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
{value}
|
||||
</Text>
|
||||
</Stack>
|
||||
));
|
||||
|
||||
const PlayerListItem = memo(({ stat, onPlayerClick, mmr, onRegisterViewport, onUnregisterViewport }: PlayerListItemProps) => {
|
||||
const viewportRef = useRef<HTMLDivElement>(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)
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Group justify="space-between" align="center" w="100%">
|
||||
<Group gap="sm" align="center">
|
||||
<Avatar name={stat.player_name} size={40} />
|
||||
<Stack gap={2}>
|
||||
<Group gap='xs'>
|
||||
<Text size="sm" fw={600}>
|
||||
{stat.player_name}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed" ta="right">
|
||||
{stat.matches}
|
||||
<Text span fw={800}>M</Text>
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed" ta="right">
|
||||
{stat.tournaments}
|
||||
<Text span fw={800}>T</Text>
|
||||
</Text>
|
||||
<Group p={0} gap="sm" align="center" w="100%" wrap="nowrap" style={{ overflow: 'hidden' }}>
|
||||
<Avatar name={stat.player_name} size={40} style={{ flexShrink: 0 }} />
|
||||
<Stack gap={2} style={{ flexGrow: 1, overflow: 'hidden', minWidth: 0 }}>
|
||||
<Group gap='xs'>
|
||||
<Text size="sm" fw={600}>
|
||||
{stat.player_name}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed" ta="right">
|
||||
{stat.matches}
|
||||
<Text span fw={800}>M</Text>
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed" ta="right">
|
||||
{stat.tournaments}
|
||||
<Text span fw={800}>T</Text>
|
||||
</Text>
|
||||
</Group>
|
||||
|
||||
<ScrollArea
|
||||
viewportRef={viewportRef}
|
||||
type="never"
|
||||
>
|
||||
<Group gap='xs' wrap="nowrap">
|
||||
<StatCell label="MMR" value={mmr.toFixed(1)} />
|
||||
<StatCell label="W" value={stat.wins} />
|
||||
<StatCell label="L" value={stat.losses} />
|
||||
<StatCell label="W%" value={`${stat.win_percentage.toFixed(1)}%`} />
|
||||
<StatCell label="AWM" value={stat.margin_of_victory?.toFixed(1) || 0} />
|
||||
<StatCell label="ALM" value={stat.margin_of_loss?.toFixed(1) || 0} />
|
||||
<StatCell label="ACPM" value={stat.avg_cups_per_match.toFixed(1)} />
|
||||
<StatCell label="ACA" value={avg_cups_against?.toFixed(1) || 0} />
|
||||
<StatCell label="CF" value={stat.total_cups_made} />
|
||||
<StatCell label="CA" value={stat.total_cups_against} />
|
||||
</Group>
|
||||
<Group gap={8} ta="center">
|
||||
<Stack gap={0}>
|
||||
<Text size="xs" c="dimmed" fw={700}>
|
||||
MMR
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
{mmr.toFixed(1)}
|
||||
</Text>
|
||||
</Stack>
|
||||
<Stack gap={0}>
|
||||
<Text size="xs" c="dimmed" fw={700}>
|
||||
W
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
{stat.wins}
|
||||
</Text>
|
||||
</Stack>
|
||||
<Stack gap={0}>
|
||||
<Text size="xs" c="dimmed" fw={700}>
|
||||
L
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
{stat.losses}
|
||||
</Text>
|
||||
</Stack>
|
||||
<Stack gap={0}>
|
||||
<Text size="xs" c="dimmed" fw={700}>
|
||||
W%
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
{stat.win_percentage.toFixed(1)}%
|
||||
</Text>
|
||||
</Stack>
|
||||
<Stack gap={0}>
|
||||
<Text size="xs" c="dimmed" fw={700}>
|
||||
AWM
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
{stat.margin_of_victory?.toFixed(1) || 0}
|
||||
</Text>
|
||||
</Stack>
|
||||
<Stack gap={0}>
|
||||
<Text size="xs" c="dimmed" fw={700}>
|
||||
AC
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
{stat.avg_cups_per_match.toFixed(1)}
|
||||
</Text>
|
||||
</Stack>
|
||||
<Stack gap={0}>
|
||||
<Text size="xs" c="dimmed" fw={700}>
|
||||
CF
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
{stat.total_cups_made}
|
||||
</Text>
|
||||
</Stack>
|
||||
<Stack gap={0}>
|
||||
<Text size="xs" c="dimmed" fw={700}>
|
||||
CA
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
{stat.total_cups_against}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Group>
|
||||
|
||||
</ScrollArea>
|
||||
</Stack>
|
||||
</Group>
|
||||
</UnstyledButton>
|
||||
</>
|
||||
@@ -160,6 +142,37 @@ const PlayerStatsTable = () => {
|
||||
direction: "desc",
|
||||
});
|
||||
|
||||
const viewportsRef = useRef<Set<HTMLDivElement>>(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 (
|
||||
<Container size="100%" px={0}>
|
||||
<Stack gap="xs">
|
||||
<Text px="md" size="10px" lh={0} c="dimmed">
|
||||
Showing {filteredAndSortedStats.length} of {playerStats.length} players
|
||||
</Text>
|
||||
<TextInput
|
||||
placeholder="Search players"
|
||||
value={search}
|
||||
@@ -269,11 +285,9 @@ const PlayerStatsTable = () => {
|
||||
/>
|
||||
|
||||
<Group px="md" justify="space-between" align="center">
|
||||
<Text size="10px" lh={0} c="dimmed">
|
||||
{filteredAndSortedStats.length} of {playerStats.length} players
|
||||
</Text>
|
||||
<Group gap="xs">
|
||||
<Text size="xs" c="dimmed">Sort:</Text>
|
||||
<Group gap="xs" w="100%">
|
||||
<div></div>
|
||||
<Text ml='auto' size="xs" c="dimmed">Sort:</Text>
|
||||
<UnstyledButton
|
||||
onClick={() => handleSort("mmr")}
|
||||
style={{ display: "flex", alignItems: "center", gap: 4 }}
|
||||
@@ -335,9 +349,15 @@ const PlayerStatsTable = () => {
|
||||
<Text size="xs" mb={2}>
|
||||
• <strong>AWM:</strong> Average Win Margin
|
||||
</Text>
|
||||
<Text size="xs" mb={2}>
|
||||
• <strong>ALM:</strong> Average Loss Margin
|
||||
</Text>
|
||||
<Text size="xs" mb={2}>
|
||||
• <strong>AC:</strong> Average Cups Per Match
|
||||
</Text>
|
||||
<Text size="xs" mb={2}>
|
||||
• <strong>ACA:</strong> Average Cups Against
|
||||
</Text>
|
||||
<Text size="xs" mb={2}>
|
||||
• <strong>CF:</strong> Cups For
|
||||
</Text>
|
||||
@@ -381,6 +401,8 @@ const PlayerStatsTable = () => {
|
||||
stat={stat}
|
||||
onPlayerClick={handlePlayerClick}
|
||||
mmr={stat.mmr}
|
||||
onRegisterViewport={handleRegisterViewport}
|
||||
onUnregisterViewport={handleUnregisterViewport}
|
||||
/>
|
||||
{index < filteredAndSortedStats.length - 1 && <Divider />}
|
||||
</Box>
|
||||
|
||||
Reference in New Issue
Block a user