more stats
This commit is contained in:
@@ -5,52 +5,66 @@ import {
|
|||||||
Container,
|
Container,
|
||||||
Divider,
|
Divider,
|
||||||
Skeleton,
|
Skeleton,
|
||||||
|
ScrollArea,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
|
|
||||||
const PlayerListItemSkeleton = () => {
|
const PlayerListItemSkeleton = () => {
|
||||||
return (
|
return (
|
||||||
<Box p="md">
|
<Box p="md">
|
||||||
<Group justify="space-between" align="center" w="100%">
|
<Group gap="sm" align="center" w="100%" wrap="nowrap" style={{ overflow: 'hidden' }}>
|
||||||
<Group gap="sm" align="center">
|
<Skeleton height={40} width={40} circle style={{ flexShrink: 0 }} />
|
||||||
<Skeleton height={45} circle />
|
<Stack gap={2} style={{ flexGrow: 1, overflow: 'hidden', minWidth: 0 }}>
|
||||||
<Stack gap={2}>
|
<Group gap='xs'>
|
||||||
<Group gap='xs'>
|
<Skeleton height={16} width={120} />
|
||||||
<Skeleton height={16} width={120} />
|
<Skeleton height={12} width={30} />
|
||||||
<Skeleton height={12} width={60} />
|
<Skeleton height={12} width={30} />
|
||||||
<Skeleton height={12} width={80} />
|
</Group>
|
||||||
</Group>
|
|
||||||
<Group gap="md" ta="center">
|
<ScrollArea type="never">
|
||||||
<Stack gap={0}>
|
<Group gap='xs' wrap="nowrap">
|
||||||
|
<Stack gap={0} style={{ flexShrink: 0 }}>
|
||||||
<Skeleton height={10} width={25} />
|
<Skeleton height={10} width={25} />
|
||||||
<Skeleton height={10} width={30} />
|
<Skeleton height={10} width={30} />
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack gap={0}>
|
<Stack gap={0} style={{ flexShrink: 0 }}>
|
||||||
<Skeleton height={10} width={10} />
|
<Skeleton height={10} width={10} />
|
||||||
<Skeleton height={10} width={15} />
|
<Skeleton height={10} width={15} />
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack gap={0}>
|
<Stack gap={0} style={{ flexShrink: 0 }}>
|
||||||
<Skeleton height={10} width={10} />
|
<Skeleton height={10} width={10} />
|
||||||
<Skeleton height={10} width={15} />
|
<Skeleton height={10} width={15} />
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack gap={0}>
|
<Stack gap={0} style={{ flexShrink: 0 }}>
|
||||||
<Skeleton height={10} width={20} />
|
<Skeleton height={10} width={20} />
|
||||||
<Skeleton height={10} width={25} />
|
<Skeleton height={10} width={25} />
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack gap={0}>
|
<Stack gap={0} style={{ flexShrink: 0 }}>
|
||||||
<Skeleton height={10} width={25} />
|
<Skeleton height={10} width={25} />
|
||||||
<Skeleton height={10} width={20} />
|
<Skeleton height={10} width={20} />
|
||||||
</Stack>
|
</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={15} />
|
||||||
<Skeleton height={10} width={25} />
|
<Skeleton height={10} width={25} />
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack gap={0}>
|
<Stack gap={0} style={{ flexShrink: 0 }}>
|
||||||
<Skeleton height={10} width={15} />
|
<Skeleton height={10} width={15} />
|
||||||
<Skeleton height={10} width={25} />
|
<Skeleton height={10} width={25} />
|
||||||
</Stack>
|
</Stack>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</ScrollArea>
|
||||||
</Group>
|
</Stack>
|
||||||
</Group>
|
</Group>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
@@ -60,13 +74,13 @@ const PlayerStatsTableSkeleton = () => {
|
|||||||
return (
|
return (
|
||||||
<Container size="100%" px={0}>
|
<Container size="100%" px={0}>
|
||||||
<Stack gap="xs">
|
<Stack gap="xs">
|
||||||
<Box px="md" pb="xs">
|
<Skeleton mx="md" height={12} width={100} />
|
||||||
|
<Box px="md" pb={4}>
|
||||||
<Skeleton height={40} />
|
<Skeleton height={40} />
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Group px="md" justify="space-between" align="center">
|
<Group justify="space-between" align="center" w='100%'>
|
||||||
<Skeleton height={12} width={100} />
|
<Group ml="auto" gap="xs">
|
||||||
<Group gap="xs">
|
|
||||||
<Skeleton height={12} width={200} />
|
<Skeleton height={12} width={200} />
|
||||||
</Group>
|
</Group>
|
||||||
</Group>
|
</Group>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useMemo, useCallback, memo, use } from "react";
|
import { useState, useMemo, useCallback, memo, useRef, useEffect } from "react";
|
||||||
import {
|
import {
|
||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
UnstyledButton,
|
UnstyledButton,
|
||||||
Popover,
|
Popover,
|
||||||
ActionIcon,
|
ActionIcon,
|
||||||
|
ScrollArea,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import {
|
import {
|
||||||
MagnifyingGlassIcon,
|
MagnifyingGlassIcon,
|
||||||
@@ -37,9 +38,41 @@ interface PlayerListItemProps {
|
|||||||
stat: PlayerStats;
|
stat: PlayerStats;
|
||||||
onPlayerClick: (playerId: string) => void;
|
onPlayerClick: (playerId: string) => void;
|
||||||
mmr: number;
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -59,92 +92,41 @@ const PlayerListItem = memo(({ stat, onPlayerClick, mmr }: PlayerListItemProps)
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Group justify="space-between" align="center" w="100%">
|
<Group p={0} gap="sm" align="center" w="100%" wrap="nowrap" style={{ overflow: 'hidden' }}>
|
||||||
<Group gap="sm" align="center">
|
<Avatar name={stat.player_name} size={40} style={{ flexShrink: 0 }} />
|
||||||
<Avatar name={stat.player_name} size={40} />
|
<Stack gap={2} style={{ flexGrow: 1, overflow: 'hidden', minWidth: 0 }}>
|
||||||
<Stack gap={2}>
|
<Group gap='xs'>
|
||||||
<Group gap='xs'>
|
<Text size="sm" fw={600}>
|
||||||
<Text size="sm" fw={600}>
|
{stat.player_name}
|
||||||
{stat.player_name}
|
</Text>
|
||||||
</Text>
|
<Text size="xs" c="dimmed" ta="right">
|
||||||
<Text size="xs" c="dimmed" ta="right">
|
{stat.matches}
|
||||||
{stat.matches}
|
<Text span fw={800}>M</Text>
|
||||||
<Text span fw={800}>M</Text>
|
</Text>
|
||||||
</Text>
|
<Text size="xs" c="dimmed" ta="right">
|
||||||
<Text size="xs" c="dimmed" ta="right">
|
{stat.tournaments}
|
||||||
{stat.tournaments}
|
<Text span fw={800}>T</Text>
|
||||||
<Text span fw={800}>T</Text>
|
</Text>
|
||||||
</Text>
|
</Group>
|
||||||
</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
|
||||||
|
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>
|
||||||
|
</ScrollArea>
|
||||||
|
</Stack>
|
||||||
</Group>
|
</Group>
|
||||||
</UnstyledButton>
|
</UnstyledButton>
|
||||||
</>
|
</>
|
||||||
@@ -160,6 +142,37 @@ const PlayerStatsTable = () => {
|
|||||||
direction: "desc",
|
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 => {
|
const calculateMMR = (stat: PlayerStats): number => {
|
||||||
if (stat.matches === 0) return 0;
|
if (stat.matches === 0) return 0;
|
||||||
|
|
||||||
@@ -259,6 +272,9 @@ const PlayerStatsTable = () => {
|
|||||||
return (
|
return (
|
||||||
<Container size="100%" px={0}>
|
<Container size="100%" px={0}>
|
||||||
<Stack gap="xs">
|
<Stack gap="xs">
|
||||||
|
<Text px="md" size="10px" lh={0} c="dimmed">
|
||||||
|
Showing {filteredAndSortedStats.length} of {playerStats.length} players
|
||||||
|
</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
placeholder="Search players"
|
placeholder="Search players"
|
||||||
value={search}
|
value={search}
|
||||||
@@ -269,11 +285,9 @@ const PlayerStatsTable = () => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Group px="md" justify="space-between" align="center">
|
<Group px="md" justify="space-between" align="center">
|
||||||
<Text size="10px" lh={0} c="dimmed">
|
<Group gap="xs" w="100%">
|
||||||
{filteredAndSortedStats.length} of {playerStats.length} players
|
<div></div>
|
||||||
</Text>
|
<Text ml='auto' size="xs" c="dimmed">Sort:</Text>
|
||||||
<Group gap="xs">
|
|
||||||
<Text size="xs" c="dimmed">Sort:</Text>
|
|
||||||
<UnstyledButton
|
<UnstyledButton
|
||||||
onClick={() => handleSort("mmr")}
|
onClick={() => handleSort("mmr")}
|
||||||
style={{ display: "flex", alignItems: "center", gap: 4 }}
|
style={{ display: "flex", alignItems: "center", gap: 4 }}
|
||||||
@@ -335,9 +349,15 @@ const PlayerStatsTable = () => {
|
|||||||
<Text size="xs" mb={2}>
|
<Text size="xs" mb={2}>
|
||||||
• <strong>AWM:</strong> Average Win Margin
|
• <strong>AWM:</strong> Average Win Margin
|
||||||
</Text>
|
</Text>
|
||||||
|
<Text size="xs" mb={2}>
|
||||||
|
• <strong>ALM:</strong> Average Loss Margin
|
||||||
|
</Text>
|
||||||
<Text size="xs" mb={2}>
|
<Text size="xs" mb={2}>
|
||||||
• <strong>AC:</strong> Average Cups Per Match
|
• <strong>AC:</strong> Average Cups Per Match
|
||||||
</Text>
|
</Text>
|
||||||
|
<Text size="xs" mb={2}>
|
||||||
|
• <strong>ACA:</strong> Average Cups Against
|
||||||
|
</Text>
|
||||||
<Text size="xs" mb={2}>
|
<Text size="xs" mb={2}>
|
||||||
• <strong>CF:</strong> Cups For
|
• <strong>CF:</strong> Cups For
|
||||||
</Text>
|
</Text>
|
||||||
@@ -381,6 +401,8 @@ const PlayerStatsTable = () => {
|
|||||||
stat={stat}
|
stat={stat}
|
||||||
onPlayerClick={handlePlayerClick}
|
onPlayerClick={handlePlayerClick}
|
||||||
mmr={stat.mmr}
|
mmr={stat.mmr}
|
||||||
|
onRegisterViewport={handleRegisterViewport}
|
||||||
|
onUnregisterViewport={handleUnregisterViewport}
|
||||||
/>
|
/>
|
||||||
{index < filteredAndSortedStats.length - 1 && <Divider />}
|
{index < filteredAndSortedStats.length - 1 && <Divider />}
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
Reference in New Issue
Block a user