more stats

This commit is contained in:
yohlo
2025-10-11 00:29:29 -05:00
parent 4b534c86cd
commit 6a7d119d3e
2 changed files with 151 additions and 115 deletions

View File

@@ -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>

View File

@@ -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>