Compare commits

2 Commits

Author SHA1 Message Date
yohlo
6a7d119d3e more stats 2025-10-11 00:29:29 -05:00
yohlo
4b534c86cd fixes, improvmeents 2025-10-10 23:44:27 -05:00
6 changed files with 235 additions and 106 deletions

View File

@@ -14,6 +14,7 @@
"@mantine/tiptap": "^8.2.4",
"@phosphor-icons/react": "^2.1.10",
"@svgmoji/noto": "^3.2.0",
"@tanstack/react-devtools": "^0.7.6",
"@tanstack/react-query": "^5.66.0",
"@tanstack/react-query-devtools": "^5.66.0",
"@tanstack/react-router": "^1.130.12",
@@ -26,6 +27,7 @@
"@tiptap/starter-kit": "^3.4.3",
"@types/bun": "^1.2.22",
"@types/ioredis": "^4.28.10",
"browser-image-compression": "^2.0.2",
"dotenv": "^17.2.2",
"embla-carousel-react": "^8.6.0",
"framer-motion": "^12.23.12",
@@ -336,6 +338,14 @@
"@simplewebauthn/browser": ["@simplewebauthn/browser@13.2.0", "", {}, "sha512-N3fuA1AAnTo5gCStYoIoiasPccC+xPLx2YU88Dv0GeAmPQTWHETlZQq5xZ0DgUq1H9loXMWQH5qqUjcI7BHJ1A=="],
"@solid-primitives/event-listener": ["@solid-primitives/event-listener@2.4.3", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-h4VqkYFv6Gf+L7SQj+Y6puigL/5DIi7x5q07VZET7AWcS+9/G3WfIE9WheniHWJs51OEkRB43w6lDys5YeFceg=="],
"@solid-primitives/keyboard": ["@solid-primitives/keyboard@1.3.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/rootless": "^1.5.2", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-9dQHTTgLBqyAI7aavtO+HnpTVJgWQA1ghBSrmLtMu1SMxLPDuLfuNr+Tk5udb4AL4Ojg7h9JrKOGEEDqsJXWJA=="],
"@solid-primitives/rootless": ["@solid-primitives/rootless@1.5.2", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-9HULb0QAzL2r47CCad0M+NKFtQ+LrGGNHZfteX/ThdGvKIg2o2GYhBooZubTCd/RTu2l2+Nw4s+dEfiDGvdrrQ=="],
"@solid-primitives/utils": ["@solid-primitives/utils@6.3.2", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-hZ/M/qr25QOCcwDPOHtGjxTD8w2mNyVAYvcfgwzBHq2RwNqHNdDNsMZYap20+ruRwW4A3Cdkczyoz0TSxLCAPQ=="],
"@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="],
"@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="],
@@ -344,6 +354,12 @@
"@svgmoji/noto": ["@svgmoji/noto@3.2.0", "", { "dependencies": { "@babel/runtime": "^7.12.5", "@svgmoji/core": "^3.2.0" } }, "sha512-JgtNciB06hMDI1Pb1N2IgLh44XRMZUUNwBANzjY5jXTPqOCu1A1VA35ENvUsRhEUZOm8I+hbdAEHkwMVqxLeIQ=="],
"@tanstack/devtools": ["@tanstack/devtools@0.6.20", "", { "dependencies": { "@solid-primitives/keyboard": "^1.3.3", "@tanstack/devtools-event-bus": "0.3.2", "@tanstack/devtools-ui": "0.4.2", "clsx": "^2.1.1", "goober": "^2.1.16", "solid-js": "^1.9.9" } }, "sha512-7Sw6bWvwKsHDNLg+8v7xOXhE5tzwx6/KgLWSSP55pJ86wpSXYdIm89vvXm4ED1lgKfEU5l3f4Y6QVagU4rgRiQ=="],
"@tanstack/devtools-event-bus": ["@tanstack/devtools-event-bus@0.3.2", "", { "dependencies": { "ws": "^8.18.3" } }, "sha512-yJT2As/drc+Epu0nsqCsJaKaLcaNGufiNxSlp/+/oeTD0jsBxF9/PJBfh66XVpYXkKr97b8689mSu7QMef0Rrw=="],
"@tanstack/devtools-ui": ["@tanstack/devtools-ui@0.4.2", "", { "dependencies": { "clsx": "^2.1.1", "goober": "^2.1.16", "solid-js": "^1.9.9" } }, "sha512-xvALRLeD+TYjaLx9f9OrRBBZITAYPIk7RH8LRiESUQHw7lZO/sBU1ggrcSePh7TwKWXl9zLmtUi+7xVIS+j/dQ=="],
"@tanstack/directive-functions-plugin": ["@tanstack/directive-functions-plugin@1.132.0", "", { "dependencies": { "@babel/code-frame": "7.27.1", "@babel/core": "^7.27.7", "@babel/traverse": "^7.27.7", "@babel/types": "^7.27.7", "@tanstack/router-utils": "1.132.0", "babel-dead-code-elimination": "^1.0.10", "tiny-invariant": "^1.3.3" }, "peerDependencies": { "vite": ">=6.0.0 || >=7.0.0" } }, "sha512-5+K3msIpSYkiDE0PTIAT2HzZRps/M2uQsDEA5HApXxOhIAWykQ/yyO1umgkMwYpgJqnT96AVHb0E559Dfvhj0A=="],
"@tanstack/history": ["@tanstack/history@1.132.0", "", {}, "sha512-GG2R9I6QSlbNR9fEuX2sQCigY6K28w51h2634TWmkaHXlzQw+rWuIWr4nAGM9doA+kWRi1LFSFMvAiG3cOqjXQ=="],
@@ -352,6 +368,8 @@
"@tanstack/query-devtools": ["@tanstack/query-devtools@5.90.1", "", {}, "sha512-GtINOPjPUH0OegJExZ70UahT9ykmAhmtNVcmtdnOZbxLwT7R5OmRztR5Ahe3/Cu7LArEmR6/588tAycuaWb1xQ=="],
"@tanstack/react-devtools": ["@tanstack/react-devtools@0.7.6", "", { "dependencies": { "@tanstack/devtools": "0.6.20" }, "peerDependencies": { "@types/react": ">=16.8", "@types/react-dom": ">=16.8", "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-fP0jY7yed0HVIEhs+rjn8wZqABD/6TUiq6SV8jlyYP8NBK2Jfq3ce+IRw5w+N7KBzEokveLQFktxoLNpt3ZOkA=="],
"@tanstack/react-query": ["@tanstack/react-query@5.90.2", "", { "dependencies": { "@tanstack/query-core": "5.90.2" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-CLABiR+h5PYfOWr/z+vWFt5VsOA2ekQeRQBFSKlcoW6Ndx/f8rfyVmq4LbgOM4GG2qtxAxjLYLOpCNTYm4uKzw=="],
"@tanstack/react-query-devtools": ["@tanstack/react-query-devtools@5.90.2", "", { "dependencies": { "@tanstack/query-devtools": "5.90.1" }, "peerDependencies": { "@tanstack/react-query": "^5.90.2", "react": "^18 || ^19" } }, "sha512-vAXJzZuBXtCQtrY3F/yUNJCV4obT/A/n81kb3+YqLbro5Z2+phdAbceO+deU3ywPw8B42oyJlp4FhO0SoivDFQ=="],
@@ -544,6 +562,8 @@
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
"browser-image-compression": ["browser-image-compression@2.0.2", "", { "dependencies": { "uzip": "0.20201231.0" } }, "sha512-pBLlQyUf6yB8SmmngrcOw3EoS4RpQ1BcylI3T9Yqn7+4nrQTXJD4sJDe5ODnJdrvNMaio5OicFo75rDyJD2Ucw=="],
"browser-tabs-lock": ["browser-tabs-lock@1.3.0", "", { "dependencies": { "lodash": ">=4.17.21" } }, "sha512-g6nHaobTiT0eMZ7jh16YpD2kcjAp+PInbiVq3M1x6KKaEIVhT4v9oURNIpZLOZ3LQbQ3XYfNhMAb/9hzNLIWrw=="],
"browserslist": ["browserslist@4.26.2", "", { "dependencies": { "baseline-browser-mapping": "^2.8.3", "caniuse-lite": "^1.0.30001741", "electron-to-chromium": "^1.5.218", "node-releases": "^2.0.21", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-ECFzp6uFOSB+dcZ5BK/IBaGWssbSYBHvuMeMt3MMFyhI0Z8SqGgEkBLARgpRH3hutIgPVsALcMwbDrJqPxQ65A=="],
@@ -1166,6 +1186,8 @@
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
"uzip": ["uzip@0.20201231.0", "", {}, "sha512-OZeJfZP+R0z9D6TmBgLq2LHzSSptGMGDGigGiEe0pr8UBe/7fdflgHlHBNDASTXB5jnFuxHpNaJywSg8YFeGng=="],
"vaul": ["vaul@1.1.2", "", { "dependencies": { "@radix-ui/react-dialog": "^1.1.1" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA=="],
"victory-vendor": ["victory-vendor@37.3.6", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ=="],
@@ -1190,6 +1212,8 @@
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
"xmlbuilder": ["xmlbuilder@13.0.2", "", {}, "sha512-Eux0i2QdDYKbdbA6AM6xE4m6ZTZr4G4xF9kahI2ukSEMCzwce2eX9WlTI5J3s+NU7hpasFsr8hWIONae7LluAQ=="],
"xmlbuilder2": ["xmlbuilder2@3.1.1", "", { "dependencies": { "@oozcitak/dom": "1.15.10", "@oozcitak/infra": "1.0.8", "@oozcitak/util": "8.3.8", "js-yaml": "3.14.1" } }, "sha512-WCSfbfZnQDdLQLiMdGUQpMxxckeQ4oZNMNhLVkcekTu7xhD4tuUDyAPoY8CwXvBYE6LwBHd6QW2WZXlOWr1vCw=="],

View File

@@ -20,6 +20,7 @@
"@mantine/tiptap": "^8.2.4",
"@phosphor-icons/react": "^2.1.10",
"@svgmoji/noto": "^3.2.0",
"@tanstack/react-devtools": "^0.7.6",
"@tanstack/react-query": "^5.66.0",
"@tanstack/react-query-devtools": "^5.66.0",
"@tanstack/react-router": "^1.130.12",

View File

@@ -1,6 +1,9 @@
import { AuthProvider } from "@/contexts/auth-context"
import { SpotifyProvider } from "@/contexts/spotify-context"
import MantineProvider from "@/lib/mantine/mantine-provider"
import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools'
import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools'
import { TanStackDevtools } from '@tanstack/react-devtools'
import { Toaster } from "sonner"
const Providers = ({ children }: { children: React.ReactNode }) => {
@@ -8,6 +11,22 @@ const Providers = ({ children }: { children: React.ReactNode }) => {
<AuthProvider>
<SpotifyProvider>
<MantineProvider>
<TanStackDevtools
eventBusConfig={{
debug: false,
connectToServerBus: true,
}}
plugins={[
{
name: 'TanStack Query',
render: <ReactQueryDevtoolsPanel />,
},
{
name: 'TanStack Router',
render: <TanStackRouterDevtoolsPanel />,
}
]}
/>
<Toaster position='top-center' />
{children}
</MantineProvider>

View File

@@ -32,7 +32,10 @@ const Pullable: React.FC<PullableProps> = ({ children, scrollPosition, onScrollP
if (refresh.length > 0) {
// TODO: Remove this after testing - or does the delay help ux?
await new Promise(resolve => setTimeout(resolve, 1000));
await queryClient.refetchQueries({ queryKey: refresh, exact: true});
refresh.forEach(async (queryKey) => {
const keyArray = Array.isArray(queryKey) ? queryKey : [queryKey];
await queryClient.refetchQueries({ queryKey: keyArray, exact: true });
});
}
setIsRefreshing(false);
}, [refresh]);

View File

@@ -5,53 +5,67 @@ 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="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={60} />
<Skeleton height={12} width={80} />
<Skeleton height={12} width={30} />
<Skeleton height={12} width={30} />
</Group>
<Group gap="md" ta="center">
<Stack gap={0}>
<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>
</ScrollArea>
</Stack>
</Group>
</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 } 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,82 +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 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} matches
{stat.matches}
<Text span fw={800}>M</Text>
</Text>
<Text size="xs" c="dimmed" ta="right">
{stat.tournaments} tournaments
{stat.tournaments}
<Text span fw={800}>T</Text>
</Text>
</Group>
<Group gap="md" 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}>
AVG
</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>
</UnstyledButton>
</>
@@ -150,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;
@@ -249,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}
@@ -259,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 }}
@@ -301,6 +325,48 @@ const PlayerStatsTable = () => {
</Popover.Target>
<Popover.Dropdown>
<Box maw={280}>
<Text size="sm" fw={500} mb="xs">
Stat Abbreviations:
</Text>
<Text size="xs" mb={2}>
<strong>M:</strong> Matches
</Text>
<Text size="xs" mb={2}>
<strong>T:</strong> Tournaments
</Text>
<Text size="xs" mb={2}>
<strong>MMR:</strong> Matchmaking Rating
</Text>
<Text size="xs" mb={2}>
<strong>W:</strong> Wins
</Text>
<Text size="xs" mb={2}>
<strong>L:</strong> Losses
</Text>
<Text size="xs" mb={2}>
<strong>W%:</strong> Win Percentage
</Text>
<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>
<Text size="xs" mb={2}>
<strong>CA:</strong> Cups Against
</Text>
<Divider my="sm" />
<Text size="sm" fw={500} mb="xs">
MMR Calculation:
</Text>
@@ -335,6 +401,8 @@ const PlayerStatsTable = () => {
stat={stat}
onPlayerClick={handlePlayerClick}
mmr={stat.mmr}
onRegisterViewport={handleRegisterViewport}
onUnregisterViewport={handleUnregisterViewport}
/>
{index < filteredAndSortedStats.length - 1 && <Divider />}
</Box>