infinite scroll
This commit is contained in:
@@ -0,0 +1,50 @@
|
|||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
|
migrate((app) => {
|
||||||
|
{
|
||||||
|
const collection = app.findCollectionByNameOrId("player_stats");
|
||||||
|
unmarshal({
|
||||||
|
"viewQuery": "SELECT\n p.id as id,\n p.id as player_id,\n (p.first_name || ' ' || p.last_name) as player_name,\n COUNT(mt.match_id) as matches,\n COUNT(DISTINCT mt.tournament) as tournaments,\n SUM(CASE WHEN mt.cups_for > mt.cups_against THEN 1 ELSE 0 END) as wins,\n SUM(CASE WHEN mt.cups_for < mt.cups_against THEN 1 ELSE 0 END) as losses,\n SUM(mt.cups_for) as total_cups_made,\n SUM(mt.cups_against) as total_cups_against,\n ROUND((CAST(SUM(CASE WHEN mt.cups_for > mt.cups_against THEN 1 ELSE 0 END) AS REAL) / COUNT(mt.match_id)) * 100, 2) as win_percentage,\n ROUND(CAST(SUM(mt.cups_for) AS REAL) / COUNT(mt.match_id), 2) as avg_cups_per_match,\n ROUND(AVG(CASE WHEN mt.cups_for > mt.cups_against THEN mt.cups_for - mt.cups_against ELSE NULL END), 2) as margin_of_victory,\n ROUND(AVG(CASE WHEN mt.cups_for < mt.cups_against THEN mt.cups_against - mt.cups_for ELSE NULL END), 2) as margin_of_loss\n FROM (\n SELECT m.id as match_id, m.tournament as tournament, m.home as team_id, m.home_cups as cups_for, m.away_cups as cups_against\n FROM matches m\n JOIN tournaments tour ON m.tournament = tour.id\n WHERE m.status = 'ended'\n UNION ALL\n SELECT m.id, m.tournament, m.away, m.away_cups, m.home_cups\n FROM matches m\n JOIN tournaments tour ON m.tournament = tour.id\n WHERE m.status = 'ended'\n ) mt\n JOIN (\n SELECT teams.id AS team_id, je.value AS player_id\n FROM teams, json_each(ifnull(nullif(teams.players, ''), '[]')) je\n GROUP BY teams.id, je.value\n ) tp ON tp.team_id = mt.team_id\n JOIN players p ON p.id = tp.player_id\n GROUP BY p.id"
|
||||||
|
}, collection);
|
||||||
|
app.save(collection);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const collection = app.findCollectionByNameOrId("player_mainline_stats");
|
||||||
|
unmarshal({
|
||||||
|
"viewQuery": "SELECT\n p.id as id,\n p.id as player_id,\n (p.first_name || ' ' || p.last_name) as player_name,\n COUNT(mt.match_id) as matches,\n COUNT(DISTINCT mt.tournament) as tournaments,\n SUM(CASE WHEN mt.cups_for > mt.cups_against THEN 1 ELSE 0 END) as wins,\n SUM(CASE WHEN mt.cups_for < mt.cups_against THEN 1 ELSE 0 END) as losses,\n SUM(mt.cups_for) as total_cups_made,\n SUM(mt.cups_against) as total_cups_against,\n ROUND((CAST(SUM(CASE WHEN mt.cups_for > mt.cups_against THEN 1 ELSE 0 END) AS REAL) / COUNT(mt.match_id)) * 100, 2) as win_percentage,\n ROUND(CAST(SUM(mt.cups_for) AS REAL) / COUNT(mt.match_id), 2) as avg_cups_per_match,\n ROUND(AVG(CASE WHEN mt.cups_for > mt.cups_against THEN mt.cups_for - mt.cups_against ELSE NULL END), 2) as margin_of_victory,\n ROUND(AVG(CASE WHEN mt.cups_for < mt.cups_against THEN mt.cups_against - mt.cups_for ELSE NULL END), 2) as margin_of_loss\n FROM (\n SELECT m.id as match_id, m.tournament as tournament, m.home as team_id, m.home_cups as cups_for, m.away_cups as cups_against\n FROM matches m\n JOIN tournaments tour ON m.tournament = tour.id\n WHERE m.status = 'ended' AND (tour.regional = false OR tour.regional IS NULL)\n UNION ALL\n SELECT m.id, m.tournament, m.away, m.away_cups, m.home_cups\n FROM matches m\n JOIN tournaments tour ON m.tournament = tour.id\n WHERE m.status = 'ended' AND (tour.regional = false OR tour.regional IS NULL)\n ) mt\n JOIN (\n SELECT teams.id AS team_id, je.value AS player_id\n FROM teams, json_each(ifnull(nullif(teams.players, ''), '[]')) je\n GROUP BY teams.id, je.value\n ) tp ON tp.team_id = mt.team_id\n JOIN players p ON p.id = tp.player_id\n GROUP BY p.id"
|
||||||
|
}, collection);
|
||||||
|
app.save(collection);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const collection = app.findCollectionByNameOrId("player_regional_stats");
|
||||||
|
unmarshal({
|
||||||
|
"viewQuery": "SELECT\n p.id as id,\n p.id as player_id,\n (p.first_name || ' ' || p.last_name) as player_name,\n COUNT(mt.match_id) as matches,\n COUNT(DISTINCT mt.tournament) as tournaments,\n SUM(CASE WHEN mt.cups_for > mt.cups_against THEN 1 ELSE 0 END) as wins,\n SUM(CASE WHEN mt.cups_for < mt.cups_against THEN 1 ELSE 0 END) as losses,\n SUM(mt.cups_for) as total_cups_made,\n SUM(mt.cups_against) as total_cups_against,\n ROUND((CAST(SUM(CASE WHEN mt.cups_for > mt.cups_against THEN 1 ELSE 0 END) AS REAL) / COUNT(mt.match_id)) * 100, 2) as win_percentage,\n ROUND(CAST(SUM(mt.cups_for) AS REAL) / COUNT(mt.match_id), 2) as avg_cups_per_match,\n ROUND(AVG(CASE WHEN mt.cups_for > mt.cups_against THEN mt.cups_for - mt.cups_against ELSE NULL END), 2) as margin_of_victory,\n ROUND(AVG(CASE WHEN mt.cups_for < mt.cups_against THEN mt.cups_against - mt.cups_for ELSE NULL END), 2) as margin_of_loss\n FROM (\n SELECT m.id as match_id, m.tournament as tournament, m.home as team_id, m.home_cups as cups_for, m.away_cups as cups_against\n FROM matches m\n JOIN tournaments tour ON m.tournament = tour.id\n WHERE m.status = 'ended' AND +tour.regional = true\n UNION ALL\n SELECT m.id, m.tournament, m.away, m.away_cups, m.home_cups\n FROM matches m\n JOIN tournaments tour ON m.tournament = tour.id\n WHERE m.status = 'ended' AND +tour.regional = true\n ) mt\n JOIN (\n SELECT teams.id AS team_id, je.value AS player_id\n FROM teams, json_each(ifnull(nullif(teams.players, ''), '[]')) je\n GROUP BY teams.id, je.value\n ) tp ON tp.team_id = mt.team_id\n JOIN players p ON p.id = tp.player_id\n GROUP BY p.id"
|
||||||
|
}, collection);
|
||||||
|
app.save(collection);
|
||||||
|
}
|
||||||
|
}, (app) => {
|
||||||
|
{
|
||||||
|
const collection = app.findCollectionByNameOrId("player_stats");
|
||||||
|
unmarshal({
|
||||||
|
"viewQuery": "SELECT\n p.id as id,\n p.id as player_id,\n (p.first_name || ' ' || p.last_name) as player_name,\n COUNT(m.id) as matches,\n COUNT(DISTINCT m.tournament) as tournaments,\n SUM(CASE\n WHEN (m.home = t.id AND m.home_cups > m.away_cups) OR\n (m.away = t.id AND m.away_cups > m.home_cups)\n THEN 1 ELSE 0\n END) as wins,\n SUM(CASE\n WHEN (m.home = t.id AND m.home_cups < m.away_cups) OR\n (m.away = t.id AND m.away_cups < m.home_cups)\n THEN 1 ELSE 0\n END) as losses,\n SUM(CASE\n WHEN m.home = t.id THEN m.home_cups\n WHEN m.away = t.id THEN m.away_cups\n ELSE 0\n END) as total_cups_made,\n SUM(CASE\n WHEN m.home = t.id THEN m.away_cups\n WHEN m.away = t.id THEN m.home_cups\n ELSE 0\n END) as total_cups_against,\n -- Win percentage\n ROUND((CAST(SUM(CASE\n WHEN (m.home = t.id AND m.home_cups > m.away_cups) OR\n (m.away = t.id AND m.away_cups > m.home_cups)\n THEN 1 ELSE 0\n END) AS REAL) / COUNT(m.id)) * 100, 2) as win_percentage,\n -- Average cups per match\n ROUND(CAST(SUM(CASE\n WHEN m.home = t.id THEN m.home_cups\n WHEN m.away = t.id THEN m.away_cups\n ELSE 0\n END) AS REAL) / COUNT(m.id), 2) as avg_cups_per_match,\n -- Margin of Victory\n ROUND(AVG(CASE\n WHEN m.home = t.id AND m.home_cups > m.away_cups\n THEN m.home_cups - m.away_cups\n WHEN m.away = t.id AND m.away_cups > m.home_cups\n THEN m.away_cups - m.home_cups\n ELSE NULL\n END), 2) as margin_of_victory,\n -- Margin of Loss\n ROUND(AVG(CASE\n WHEN m.home = t.id AND m.home_cups < m.away_cups\n THEN m.away_cups - m.home_cups\n WHEN m.away = t.id AND m.away_cups < m.home_cups\n THEN m.home_cups - m.away_cups\n ELSE NULL\n END), 2) as margin_of_loss\n FROM players p, teams t, matches m, tournaments tour\n WHERE\n t.players LIKE '%\"' || p.id || '\"%' AND\n (m.home = t.id OR m.away = t.id) AND\n m.tournament = tour.id AND\n m.status = 'ended'\n GROUP BY p.id"
|
||||||
|
}, collection);
|
||||||
|
app.save(collection);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const collection = app.findCollectionByNameOrId("player_mainline_stats");
|
||||||
|
unmarshal({
|
||||||
|
"viewQuery": "SELECT\n p.id as id,\n p.id as player_id,\n (p.first_name || ' ' || p.last_name) as player_name,\n COUNT(m.id) as matches,\n COUNT(DISTINCT m.tournament) as tournaments,\n SUM(CASE\n WHEN (m.home = t.id AND m.home_cups > m.away_cups) OR\n (m.away = t.id AND m.away_cups > m.home_cups)\n THEN 1 ELSE 0\n END) as wins,\n SUM(CASE\n WHEN (m.home = t.id AND m.home_cups < m.away_cups) OR\n (m.away = t.id AND m.away_cups < m.home_cups)\n THEN 1 ELSE 0\n END) as losses,\n SUM(CASE\n WHEN m.home = t.id THEN m.home_cups\n WHEN m.away = t.id THEN m.away_cups\n ELSE 0\n END) as total_cups_made,\n SUM(CASE\n WHEN m.home = t.id THEN m.away_cups\n WHEN m.away = t.id THEN m.home_cups\n ELSE 0\n END) as total_cups_against,\n -- Win percentage\n ROUND((CAST(SUM(CASE\n WHEN (m.home = t.id AND m.home_cups > m.away_cups) OR\n (m.away = t.id AND m.away_cups > m.home_cups)\n THEN 1 ELSE 0\n END) AS REAL) / COUNT(m.id)) * 100, 2) as win_percentage,\n -- Average cups per match\n ROUND(CAST(SUM(CASE\n WHEN m.home = t.id THEN m.home_cups\n WHEN m.away = t.id THEN m.away_cups\n ELSE 0\n END) AS REAL) / COUNT(m.id), 2) as avg_cups_per_match,\n -- Margin of Victory\n ROUND(AVG(CASE\n WHEN m.home = t.id AND m.home_cups > m.away_cups\n THEN m.home_cups - m.away_cups\n WHEN m.away = t.id AND m.away_cups > m.home_cups\n THEN m.away_cups - m.home_cups\n ELSE NULL\n END), 2) as margin_of_victory,\n -- Margin of Loss\n ROUND(AVG(CASE\n WHEN m.home = t.id AND m.home_cups < m.away_cups\n THEN m.away_cups - m.home_cups\n WHEN m.away = t.id AND m.away_cups < m.home_cups\n THEN m.home_cups - m.away_cups\n ELSE NULL\n END), 2) as margin_of_loss\n FROM players p, teams t, matches m, tournaments tour\n WHERE\n t.players LIKE '%\"' || p.id || '\"%' AND\n (m.home = t.id OR m.away = t.id) AND\n m.tournament = tour.id AND\n m.status = 'ended' AND\n (tour.regional = false OR tour.regional IS NULL)\n GROUP BY p.id"
|
||||||
|
}, collection);
|
||||||
|
app.save(collection);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const collection = app.findCollectionByNameOrId("player_regional_stats");
|
||||||
|
unmarshal({
|
||||||
|
"viewQuery": "SELECT\n p.id as id,\n p.id as player_id,\n (p.first_name || ' ' || p.last_name) as player_name,\n COUNT(m.id) as matches,\n COUNT(DISTINCT m.tournament) as tournaments,\n SUM(CASE\n WHEN (m.home = t.id AND m.home_cups > m.away_cups) OR\n (m.away = t.id AND m.away_cups > m.home_cups)\n THEN 1 ELSE 0\n END) as wins,\n SUM(CASE\n WHEN (m.home = t.id AND m.home_cups < m.away_cups) OR\n (m.away = t.id AND m.away_cups < m.home_cups)\n THEN 1 ELSE 0\n END) as losses,\n SUM(CASE\n WHEN m.home = t.id THEN m.home_cups\n WHEN m.away = t.id THEN m.away_cups\n ELSE 0\n END) as total_cups_made,\n SUM(CASE\n WHEN m.home = t.id THEN m.away_cups\n WHEN m.away = t.id THEN m.home_cups\n ELSE 0\n END) as total_cups_against,\n -- Win percentage\n ROUND((CAST(SUM(CASE\n WHEN (m.home = t.id AND m.home_cups > m.away_cups) OR\n (m.away = t.id AND m.away_cups > m.home_cups)\n THEN 1 ELSE 0\n END) AS REAL) / COUNT(m.id)) * 100, 2) as win_percentage,\n -- Average cups per match\n ROUND(CAST(SUM(CASE\n WHEN m.home = t.id THEN m.home_cups\n WHEN m.away = t.id THEN m.away_cups\n ELSE 0\n END) AS REAL) / COUNT(m.id), 2) as avg_cups_per_match,\n -- Margin of Victory\n ROUND(AVG(CASE\n WHEN m.home = t.id AND m.home_cups > m.away_cups\n THEN m.home_cups - m.away_cups\n WHEN m.away = t.id AND m.away_cups > m.home_cups\n THEN m.away_cups - m.home_cups\n ELSE NULL\n END), 2) as margin_of_victory,\n -- Margin of Loss\n ROUND(AVG(CASE\n WHEN m.home = t.id AND m.home_cups < m.away_cups\n THEN m.away_cups - m.home_cups\n WHEN m.away = t.id AND m.away_cups < m.home_cups\n THEN m.home_cups - m.away_cups\n ELSE NULL\n END), 2) as margin_of_loss\n FROM players p, teams t, matches m, tournaments tour\n WHERE\n t.players LIKE '%\"' || p.id || '\"%' AND\n (m.home = t.id OR m.away = t.id) AND\n m.tournament = tour.id AND\n m.status = 'ended' AND\n tour.regional = true\n GROUP BY p.id"
|
||||||
|
}, collection);
|
||||||
|
app.save(collection);
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -1,19 +1,18 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import { playerQueries } from "@/features/players/queries";
|
import { playerQueries } from "@/features/players/queries";
|
||||||
import PlayerStatsTable from "@/features/players/components/player-stats-table";
|
import PlayerStatsTable from "@/features/players/components/player-stats-table";
|
||||||
import { Suspense, useState, useDeferredValue } from "react";
|
import { Suspense, useState, useDeferredValue, useEffect } from "react";
|
||||||
import PlayerStatsTableSkeleton from "@/features/players/components/player-stats-table-skeleton";
|
import PlayerStatsTableSkeleton from "@/features/players/components/player-stats-table-skeleton";
|
||||||
import { prefetchServerQuery } from "@/lib/tanstack-query/utils/prefetch";
|
import { prefetchServerQuery } from "@/lib/tanstack-query/utils/prefetch";
|
||||||
import LeagueHeadToHead from "@/features/players/components/league-head-to-head";
|
import LeagueHeadToHead from "@/features/players/components/league-head-to-head";
|
||||||
import { Box, Loader, Tabs, Button, Group, Container, Stack } from "@mantine/core";
|
import { Box, Loader, Tabs, Button, Group, Container, Stack } from "@mantine/core";
|
||||||
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
export const Route = createFileRoute("/_authed/stats")({
|
export const Route = createFileRoute("/_authed/stats")({
|
||||||
component: Stats,
|
component: Stats,
|
||||||
beforeLoad: ({ context }) => {
|
beforeLoad: ({ context }) => {
|
||||||
const queryClient = context.queryClient;
|
const queryClient = context.queryClient;
|
||||||
prefetchServerQuery(queryClient, playerQueries.allStats('all'));
|
prefetchServerQuery(queryClient, playerQueries.allStats('all'));
|
||||||
prefetchServerQuery(queryClient, playerQueries.allStats('mainline'));
|
|
||||||
prefetchServerQuery(queryClient, playerQueries.allStats('regional'));
|
|
||||||
},
|
},
|
||||||
loader: () => ({
|
loader: () => ({
|
||||||
withPadding: false,
|
withPadding: false,
|
||||||
@@ -29,6 +28,15 @@ function Stats() {
|
|||||||
const [viewType, setViewType] = useState<'all' | 'mainline' | 'regional'>('all');
|
const [viewType, setViewType] = useState<'all' | 'mainline' | 'regional'>('all');
|
||||||
const deferredViewType = useDeferredValue(viewType);
|
const deferredViewType = useDeferredValue(viewType);
|
||||||
const isStale = viewType !== deferredViewType;
|
const isStale = viewType !== deferredViewType;
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timeout = window.setTimeout(() => {
|
||||||
|
prefetchServerQuery(queryClient, playerQueries.allStats('mainline'));
|
||||||
|
prefetchServerQuery(queryClient, playerQueries.allStats('regional'));
|
||||||
|
}, 1500);
|
||||||
|
return () => window.clearTimeout(timeout);
|
||||||
|
}, [queryClient]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs defaultValue="stats">
|
<Tabs defaultValue="stats">
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import { ReactNode, useEffect, useRef, useState } from "react";
|
||||||
|
import { Box } from "@mantine/core";
|
||||||
|
|
||||||
|
interface InfiniteScrollProps<T> {
|
||||||
|
items: T[];
|
||||||
|
renderItem: (item: T, index: number) => ReactNode;
|
||||||
|
batchSize?: number;
|
||||||
|
initialCount?: number;
|
||||||
|
rootMargin?: string;
|
||||||
|
loader?: ReactNode;
|
||||||
|
hasMore?: boolean;
|
||||||
|
loading?: boolean;
|
||||||
|
onLoadMore?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const InfiniteScroll = <T,>({
|
||||||
|
items,
|
||||||
|
renderItem,
|
||||||
|
batchSize = 25,
|
||||||
|
initialCount = batchSize,
|
||||||
|
rootMargin = "600px 0px",
|
||||||
|
loader,
|
||||||
|
hasMore = false,
|
||||||
|
loading = false,
|
||||||
|
onLoadMore,
|
||||||
|
}: InfiniteScrollProps<T>) => {
|
||||||
|
const [visibleCount, setVisibleCount] = useState(initialCount);
|
||||||
|
const [prevItems, setPrevItems] = useState(items);
|
||||||
|
const sentinelRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
if (prevItems !== items) {
|
||||||
|
setPrevItems(items);
|
||||||
|
setVisibleCount(initialCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasHiddenItems = visibleCount < items.length;
|
||||||
|
const showLoader = hasHiddenItems || hasMore || loading;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const sentinel = sentinelRef.current;
|
||||||
|
if (!sentinel) return;
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
if (!entries.some((entry) => entry.isIntersecting)) return;
|
||||||
|
|
||||||
|
if (visibleCount < items.length) {
|
||||||
|
setVisibleCount((count) => Math.min(count + batchSize, items.length));
|
||||||
|
} else if (hasMore && !loading) {
|
||||||
|
onLoadMore?.();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ rootMargin }
|
||||||
|
);
|
||||||
|
|
||||||
|
observer.observe(sentinel);
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, [items, visibleCount, batchSize, hasMore, loading, onLoadMore, rootMargin]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{items.slice(0, visibleCount).map((item, index) => renderItem(item, index))}
|
||||||
|
{showLoader && (
|
||||||
|
<>
|
||||||
|
<Box ref={sentinelRef} />
|
||||||
|
{loader}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InfiniteScroll;
|
||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
ScrollArea,
|
ScrollArea,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
|
|
||||||
const PlayerListItemSkeleton = () => {
|
export const PlayerListItemSkeleton = () => {
|
||||||
return (
|
return (
|
||||||
<Box p="md">
|
<Box p="md">
|
||||||
<Group gap="sm" align="center" w="100%" wrap="nowrap" style={{ overflow: 'hidden' }}>
|
<Group gap="sm" align="center" w="100%" wrap="nowrap" style={{ overflow: 'hidden' }}>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useMemo, useCallback, memo, useRef, useEffect } from "react";
|
import { useState, useMemo, useCallback, memo, useRef, useEffect, useDeferredValue } from "react";
|
||||||
import {
|
import {
|
||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
@@ -23,8 +23,10 @@ import {
|
|||||||
} from "@phosphor-icons/react";
|
} from "@phosphor-icons/react";
|
||||||
import { PlayerStats } from "../types";
|
import { PlayerStats } from "../types";
|
||||||
import PlayerAvatar from "@/components/player-avatar";
|
import PlayerAvatar from "@/components/player-avatar";
|
||||||
|
import InfiniteScroll from "@/components/infinite-scroll";
|
||||||
import { useNavigate } from "@tanstack/react-router";
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
import { useAllPlayerStats } from "../queries";
|
import { useAllPlayerStats } from "../queries";
|
||||||
|
import { PlayerListItemSkeleton } from "./player-stats-table-skeleton";
|
||||||
|
|
||||||
type SortKey = keyof PlayerStats | "mmr";
|
type SortKey = keyof PlayerStats | "mmr";
|
||||||
type SortDirection = "asc" | "desc";
|
type SortDirection = "asc" | "desc";
|
||||||
@@ -146,10 +148,32 @@ interface PlayerStatsTableProps {
|
|||||||
viewType?: 'all' | 'mainline' | 'regional';
|
viewType?: 'all' | 'mainline' | 'regional';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const calculateMMR = (stat: PlayerStats): number => {
|
||||||
|
if (stat.matches === 0) return 0;
|
||||||
|
|
||||||
|
const winScore = stat.win_percentage;
|
||||||
|
const matchConfidence = Math.min(stat.matches / 15, 1);
|
||||||
|
const avgCupsScore = Math.min(stat.avg_cups_per_match * 10, 100);
|
||||||
|
const marginScore = stat.margin_of_victory
|
||||||
|
? Math.min(stat.margin_of_victory * 20, 50)
|
||||||
|
: 0;
|
||||||
|
const volumeBonus = Math.min(stat.matches * 0.5, 10);
|
||||||
|
|
||||||
|
const baseMMR =
|
||||||
|
winScore * 0.5 +
|
||||||
|
avgCupsScore * 0.25 +
|
||||||
|
marginScore * 0.15 +
|
||||||
|
volumeBonus * 0.1;
|
||||||
|
|
||||||
|
const finalMMR = baseMMR * matchConfidence;
|
||||||
|
return Math.round(finalMMR * 10) / 10;
|
||||||
|
};
|
||||||
|
|
||||||
const PlayerStatsTable = ({ viewType = 'all' }: PlayerStatsTableProps) => {
|
const PlayerStatsTable = ({ viewType = 'all' }: PlayerStatsTableProps) => {
|
||||||
const { data: playerStats } = useAllPlayerStats(viewType);
|
const { data: playerStats } = useAllPlayerStats(viewType);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
|
const deferredSearch = useDeferredValue(search);
|
||||||
const [sortConfig, setSortConfig] = useState<SortConfig>({
|
const [sortConfig, setSortConfig] = useState<SortConfig>({
|
||||||
key: "mmr" as SortKey,
|
key: "mmr" as SortKey,
|
||||||
direction: "desc",
|
direction: "desc",
|
||||||
@@ -159,10 +183,15 @@ const PlayerStatsTable = ({ viewType = 'all' }: PlayerStatsTableProps) => {
|
|||||||
const scrollHandlersRef = useRef<Map<HTMLDivElement, (e: Event) => void>>(new Map());
|
const scrollHandlersRef = useRef<Map<HTMLDivElement, (e: Event) => void>>(new Map());
|
||||||
const scrollLeaderRef = useRef<HTMLDivElement | null>(null);
|
const scrollLeaderRef = useRef<HTMLDivElement | null>(null);
|
||||||
const scrollTimeoutRef = useRef<number | null>(null);
|
const scrollTimeoutRef = useRef<number | null>(null);
|
||||||
|
const lastScrollLeftRef = useRef(0);
|
||||||
|
|
||||||
const handleRegisterViewport = useCallback((viewport: HTMLDivElement) => {
|
const handleRegisterViewport = useCallback((viewport: HTMLDivElement) => {
|
||||||
viewportsRef.current.add(viewport);
|
viewportsRef.current.add(viewport);
|
||||||
|
|
||||||
|
if (lastScrollLeftRef.current > 0) {
|
||||||
|
viewport.scrollLeft = lastScrollLeftRef.current;
|
||||||
|
}
|
||||||
|
|
||||||
const handleScrollStart = () => {
|
const handleScrollStart = () => {
|
||||||
scrollLeaderRef.current = viewport;
|
scrollLeaderRef.current = viewport;
|
||||||
};
|
};
|
||||||
@@ -179,6 +208,7 @@ const PlayerStatsTable = ({ viewType = 'all' }: PlayerStatsTableProps) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const scrollLeft = target.scrollLeft;
|
const scrollLeft = target.scrollLeft;
|
||||||
|
lastScrollLeftRef.current = scrollLeft;
|
||||||
|
|
||||||
viewportsRef.current.forEach((vp) => {
|
viewportsRef.current.forEach((vp) => {
|
||||||
if (vp !== target && Math.abs(vp.scrollLeft - scrollLeft) > 0.5) {
|
if (vp !== target && Math.abs(vp.scrollLeft - scrollLeft) > 0.5) {
|
||||||
@@ -213,27 +243,6 @@ const PlayerStatsTable = ({ viewType = 'all' }: PlayerStatsTableProps) => {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const calculateMMR = (stat: PlayerStats): number => {
|
|
||||||
if (stat.matches === 0) return 0;
|
|
||||||
|
|
||||||
const winScore = stat.win_percentage;
|
|
||||||
const matchConfidence = Math.min(stat.matches / 15, 1);
|
|
||||||
const avgCupsScore = Math.min(stat.avg_cups_per_match * 10, 100);
|
|
||||||
const marginScore = stat.margin_of_victory
|
|
||||||
? Math.min(stat.margin_of_victory * 20, 50)
|
|
||||||
: 0;
|
|
||||||
const volumeBonus = Math.min(stat.matches * 0.5, 10);
|
|
||||||
|
|
||||||
const baseMMR =
|
|
||||||
winScore * 0.5 +
|
|
||||||
avgCupsScore * 0.25 +
|
|
||||||
marginScore * 0.15 +
|
|
||||||
volumeBonus * 0.1;
|
|
||||||
|
|
||||||
const finalMMR = baseMMR * matchConfidence;
|
|
||||||
return Math.round(finalMMR * 10) / 10;
|
|
||||||
};
|
|
||||||
|
|
||||||
const statsWithMMR = useMemo(() => {
|
const statsWithMMR = useMemo(() => {
|
||||||
return playerStats.map((stat) => ({
|
return playerStats.map((stat) => ({
|
||||||
...stat,
|
...stat,
|
||||||
@@ -243,7 +252,7 @@ const PlayerStatsTable = ({ viewType = 'all' }: PlayerStatsTableProps) => {
|
|||||||
|
|
||||||
const filteredAndSortedStats = useMemo(() => {
|
const filteredAndSortedStats = useMemo(() => {
|
||||||
let filtered = statsWithMMR.filter((stat) =>
|
let filtered = statsWithMMR.filter((stat) =>
|
||||||
stat.player_name.toLowerCase().includes(search.toLowerCase())
|
stat.player_name.toLowerCase().includes(deferredSearch.toLowerCase())
|
||||||
);
|
);
|
||||||
|
|
||||||
return filtered.sort((a, b) => {
|
return filtered.sort((a, b) => {
|
||||||
@@ -272,7 +281,7 @@ const PlayerStatsTable = ({ viewType = 'all' }: PlayerStatsTableProps) => {
|
|||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
});
|
});
|
||||||
}, [statsWithMMR, search, sortConfig]);
|
}, [statsWithMMR, deferredSearch, sortConfig]);
|
||||||
|
|
||||||
const handlePlayerClick = useCallback((playerId: string) => {
|
const handlePlayerClick = useCallback((playerId: string) => {
|
||||||
navigate({ to: `/profile/${playerId}` });
|
navigate({ to: `/profile/${playerId}` });
|
||||||
@@ -433,18 +442,30 @@ const PlayerStatsTable = ({ viewType = 'all' }: PlayerStatsTableProps) => {
|
|||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<Stack gap={0}>
|
<Stack gap={0}>
|
||||||
{filteredAndSortedStats.map((stat, index) => (
|
<InfiniteScroll
|
||||||
<Box key={stat.id}>
|
items={filteredAndSortedStats}
|
||||||
<PlayerListItem
|
batchSize={25}
|
||||||
stat={stat}
|
renderItem={(stat, index) => (
|
||||||
onPlayerClick={handlePlayerClick}
|
<Box key={stat.id}>
|
||||||
mmr={stat.mmr}
|
{index > 0 && <Divider />}
|
||||||
onRegisterViewport={handleRegisterViewport}
|
<PlayerListItem
|
||||||
onUnregisterViewport={handleUnregisterViewport}
|
stat={stat}
|
||||||
/>
|
onPlayerClick={handlePlayerClick}
|
||||||
{index < filteredAndSortedStats.length - 1 && <Divider />}
|
mmr={stat.mmr}
|
||||||
</Box>
|
onRegisterViewport={handleRegisterViewport}
|
||||||
))}
|
onUnregisterViewport={handleUnregisterViewport}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
loader={
|
||||||
|
<>
|
||||||
|
<Divider />
|
||||||
|
<PlayerListItemSkeleton />
|
||||||
|
<Divider />
|
||||||
|
<PlayerListItemSkeleton />
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
{filteredAndSortedStats.length === 0 && search && (
|
{filteredAndSortedStats.length === 0 && search && (
|
||||||
|
|||||||
Reference in New Issue
Block a user