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

View File

@@ -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>
<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>
<Group gap={8} ta="center"> </ScrollArea>
<Stack gap={0}> </Stack>
<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>
</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>