skeletons
This commit is contained in:
28
pb_migrations/1758575563_updated_free_agents.js
Normal file
28
pb_migrations/1758575563_updated_free_agents.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
|
migrate((app) => {
|
||||||
|
const collection = app.findCollectionByNameOrId("pbc_2929550049")
|
||||||
|
|
||||||
|
// update collection data
|
||||||
|
unmarshal({
|
||||||
|
"createRule": "",
|
||||||
|
"deleteRule": "",
|
||||||
|
"listRule": "",
|
||||||
|
"updateRule": "",
|
||||||
|
"viewRule": ""
|
||||||
|
}, collection)
|
||||||
|
|
||||||
|
return app.save(collection)
|
||||||
|
}, (app) => {
|
||||||
|
const collection = app.findCollectionByNameOrId("pbc_2929550049")
|
||||||
|
|
||||||
|
// update collection data
|
||||||
|
unmarshal({
|
||||||
|
"createRule": null,
|
||||||
|
"deleteRule": null,
|
||||||
|
"listRule": null,
|
||||||
|
"updateRule": null,
|
||||||
|
"viewRule": null
|
||||||
|
}, collection)
|
||||||
|
|
||||||
|
return app.save(collection)
|
||||||
|
})
|
||||||
28
pb_migrations/1758575597_updated_free_agents.js
Normal file
28
pb_migrations/1758575597_updated_free_agents.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
|
migrate((app) => {
|
||||||
|
const collection = app.findCollectionByNameOrId("pbc_2929550049")
|
||||||
|
|
||||||
|
// update collection data
|
||||||
|
unmarshal({
|
||||||
|
"createRule": null,
|
||||||
|
"deleteRule": null,
|
||||||
|
"listRule": null,
|
||||||
|
"updateRule": null,
|
||||||
|
"viewRule": null
|
||||||
|
}, collection)
|
||||||
|
|
||||||
|
return app.save(collection)
|
||||||
|
}, (app) => {
|
||||||
|
const collection = app.findCollectionByNameOrId("pbc_2929550049")
|
||||||
|
|
||||||
|
// update collection data
|
||||||
|
unmarshal({
|
||||||
|
"createRule": "",
|
||||||
|
"deleteRule": "",
|
||||||
|
"listRule": "",
|
||||||
|
"updateRule": "",
|
||||||
|
"viewRule": ""
|
||||||
|
}, collection)
|
||||||
|
|
||||||
|
return app.save(collection)
|
||||||
|
})
|
||||||
@@ -1,13 +1,15 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import { playerQueries, useAllPlayerStats } from "@/features/players/queries";
|
import { playerQueries } from "@/features/players/queries";
|
||||||
import { ensureServerQueryData } from "@/lib/tanstack-query/utils/ensure";
|
|
||||||
import PlayerStatsTable from "@/features/players/components/player-stats-table";
|
import PlayerStatsTable from "@/features/players/components/player-stats-table";
|
||||||
|
import { Suspense } from "react";
|
||||||
|
import PlayerStatsTableSkeleton from "@/features/players/components/player-stats-table-skeleton";
|
||||||
|
import { prefetchServerQuery } from "@/lib/tanstack-query/utils/prefetch";
|
||||||
|
|
||||||
export const Route = createFileRoute("/_authed/stats")({
|
export const Route = createFileRoute("/_authed/stats")({
|
||||||
component: Stats,
|
component: Stats,
|
||||||
beforeLoad: async ({ context }) => {
|
beforeLoad: ({ context }) => {
|
||||||
const queryClient = context.queryClient;
|
const queryClient = context.queryClient;
|
||||||
ensureServerQueryData(queryClient, playerQueries.allStats());
|
prefetchServerQuery(queryClient, playerQueries.allStats());
|
||||||
},
|
},
|
||||||
loader: () => ({
|
loader: () => ({
|
||||||
withPadding: false,
|
withPadding: false,
|
||||||
@@ -20,7 +22,7 @@ export const Route = createFileRoute("/_authed/stats")({
|
|||||||
});
|
});
|
||||||
|
|
||||||
function Stats() {
|
function Stats() {
|
||||||
const { data: playerStats } = useAllPlayerStats();
|
return <Suspense fallback={<PlayerStatsTableSkeleton />}>
|
||||||
|
<PlayerStatsTable />
|
||||||
return <PlayerStatsTable playerStats={playerStats} />;
|
</Suspense>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import {
|
|||||||
ArrowUpIcon,
|
ArrowUpIcon,
|
||||||
ArrowDownIcon,
|
ArrowDownIcon,
|
||||||
} from "@phosphor-icons/react";
|
} from "@phosphor-icons/react";
|
||||||
import { BaseStats } from "@/shared/types/stats";
|
import { BaseStats } from "@/types/stats";
|
||||||
|
|
||||||
interface StatsOverviewProps {
|
interface StatsOverviewProps {
|
||||||
statsData: BaseStats | null;
|
statsData: BaseStats | null;
|
||||||
@@ -51,17 +51,13 @@ const StatItem = ({
|
|||||||
</Text>
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
<Text size="sm" fw={700} c="dimmed">
|
<Text size="sm" fw={700} c="dimmed">
|
||||||
{value !== null ? `${value}${suffix}` : "—"}
|
{value !== null ? `${value}${suffix}` : <Skeleton width={20} height={20} />}
|
||||||
</Text>
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const StatsOverview = ({ statsData, isLoading = false }: StatsOverviewProps) => {
|
const StatsOverview = ({ statsData, isLoading = false }: StatsOverviewProps) => {
|
||||||
if (isLoading || (!statsData && isLoading)) {
|
|
||||||
return <StatsSkeleton />
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!statsData && !isLoading) {
|
if (!statsData && !isLoading) {
|
||||||
return (
|
return (
|
||||||
<Box p="sm" h="auto" mih={200}>
|
<Box p="sm" h="auto" mih={200}>
|
||||||
@@ -126,7 +122,7 @@ const StatsOverview = ({ statsData, isLoading = false }: StatsOverviewProps) =>
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const StatsSkeleton = () => {
|
export const StatsSkeleton = () => {
|
||||||
const skeletonStats = [
|
const skeletonStats = [
|
||||||
{ label: "Matches Played", Icon: BoxingGloveIcon },
|
{ label: "Matches Played", Icon: BoxingGloveIcon },
|
||||||
{ label: "Wins", Icon: CrownIcon },
|
{ label: "Wins", Icon: CrownIcon },
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import {
|
||||||
|
Stack,
|
||||||
|
Group,
|
||||||
|
Box,
|
||||||
|
Container,
|
||||||
|
Divider,
|
||||||
|
Skeleton,
|
||||||
|
} 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}>
|
||||||
|
<Skeleton height={10} width={25} />
|
||||||
|
<Skeleton height={10} width={30} />
|
||||||
|
</Stack>
|
||||||
|
<Stack gap={0}>
|
||||||
|
<Skeleton height={10} width={10} />
|
||||||
|
<Skeleton height={10} width={15} />
|
||||||
|
</Stack>
|
||||||
|
<Stack gap={0}>
|
||||||
|
<Skeleton height={10} width={10} />
|
||||||
|
<Skeleton height={10} width={15} />
|
||||||
|
</Stack>
|
||||||
|
<Stack gap={0}>
|
||||||
|
<Skeleton height={10} width={20} />
|
||||||
|
<Skeleton height={10} width={25} />
|
||||||
|
</Stack>
|
||||||
|
<Stack gap={0}>
|
||||||
|
<Skeleton height={10} width={25} />
|
||||||
|
<Skeleton height={10} width={20} />
|
||||||
|
</Stack>
|
||||||
|
<Stack gap={0}>
|
||||||
|
<Skeleton height={10} width={15} />
|
||||||
|
<Skeleton height={10} width={25} />
|
||||||
|
</Stack>
|
||||||
|
<Stack gap={0}>
|
||||||
|
<Skeleton height={10} width={15} />
|
||||||
|
<Skeleton height={10} width={25} />
|
||||||
|
</Stack>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const PlayerStatsTableSkeleton = () => {
|
||||||
|
return (
|
||||||
|
<Container size="100%" px={0}>
|
||||||
|
<Stack gap="xs">
|
||||||
|
<Box px="md" pb="xs">
|
||||||
|
<Skeleton height={40} />
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Group px="md" justify="space-between" align="center">
|
||||||
|
<Skeleton height={12} width={100} />
|
||||||
|
<Group gap="xs">
|
||||||
|
<Skeleton height={12} width={200} />
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Stack>
|
||||||
|
{Array(10).fill(null).map((_, index) => (
|
||||||
|
<Box key={index}>
|
||||||
|
<PlayerListItemSkeleton />
|
||||||
|
{index < 9 && <Divider />}
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PlayerStatsTableSkeleton;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useMemo } from "react";
|
import { useState, useMemo, useCallback, memo } from "react";
|
||||||
import {
|
import {
|
||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
@@ -12,7 +12,6 @@ import {
|
|||||||
UnstyledButton,
|
UnstyledButton,
|
||||||
Popover,
|
Popover,
|
||||||
ActionIcon,
|
ActionIcon,
|
||||||
Skeleton,
|
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import {
|
import {
|
||||||
MagnifyingGlassIcon,
|
MagnifyingGlassIcon,
|
||||||
@@ -24,10 +23,7 @@ import {
|
|||||||
import { PlayerStats } from "../types";
|
import { PlayerStats } from "../types";
|
||||||
import Avatar from "@/components/avatar";
|
import Avatar from "@/components/avatar";
|
||||||
import { useNavigate } from "@tanstack/react-router";
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
|
import { useAllPlayerStats } from "../queries";
|
||||||
interface PlayerStatsTableProps {
|
|
||||||
playerStats: PlayerStats[];
|
|
||||||
}
|
|
||||||
|
|
||||||
type SortKey = keyof PlayerStats | "mmr";
|
type SortKey = keyof PlayerStats | "mmr";
|
||||||
type SortDirection = "asc" | "desc";
|
type SortDirection = "asc" | "desc";
|
||||||
@@ -39,33 +35,11 @@ interface SortConfig {
|
|||||||
|
|
||||||
interface PlayerListItemProps {
|
interface PlayerListItemProps {
|
||||||
stat: PlayerStats;
|
stat: PlayerStats;
|
||||||
index: number;
|
|
||||||
onPlayerClick: (playerId: string) => void;
|
onPlayerClick: (playerId: string) => void;
|
||||||
|
mmr: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PlayerListItem = ({ stat, index, onPlayerClick }: PlayerListItemProps) => {
|
const PlayerListItem = memo(({ stat, onPlayerClick, mmr }: PlayerListItemProps) => {
|
||||||
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 mmr = calculateMMR(stat);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -165,9 +139,12 @@ const PlayerListItem = ({ stat, index, onPlayerClick }: PlayerListItemProps) =>
|
|||||||
</UnstyledButton>
|
</UnstyledButton>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
const PlayerStatsTable = ({ playerStats }: PlayerStatsTableProps) => {
|
PlayerListItem.displayName = 'PlayerListItem';
|
||||||
|
|
||||||
|
const PlayerStatsTable = () => {
|
||||||
|
const { data: playerStats } = useAllPlayerStats();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const [sortConfig, setSortConfig] = useState<SortConfig>({
|
const [sortConfig, setSortConfig] = useState<SortConfig>({
|
||||||
@@ -196,8 +173,15 @@ const PlayerStatsTable = ({ playerStats }: PlayerStatsTableProps) => {
|
|||||||
return Math.round(finalMMR * 10) / 10;
|
return Math.round(finalMMR * 10) / 10;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const statsWithMMR = useMemo(() => {
|
||||||
|
return playerStats.map((stat) => ({
|
||||||
|
...stat,
|
||||||
|
mmr: calculateMMR(stat),
|
||||||
|
}));
|
||||||
|
}, [playerStats]);
|
||||||
|
|
||||||
const filteredAndSortedStats = useMemo(() => {
|
const filteredAndSortedStats = useMemo(() => {
|
||||||
let filtered = playerStats.filter((stat) =>
|
let filtered = statsWithMMR.filter((stat) =>
|
||||||
stat.player_name.toLowerCase().includes(search.toLowerCase())
|
stat.player_name.toLowerCase().includes(search.toLowerCase())
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -206,8 +190,8 @@ const PlayerStatsTable = ({ playerStats }: PlayerStatsTableProps) => {
|
|||||||
let bValue: number | string;
|
let bValue: number | string;
|
||||||
|
|
||||||
if (sortConfig.key === "mmr") {
|
if (sortConfig.key === "mmr") {
|
||||||
aValue = calculateMMR(a);
|
aValue = a.mmr;
|
||||||
bValue = calculateMMR(b);
|
bValue = b.mmr;
|
||||||
} else {
|
} else {
|
||||||
aValue = a[sortConfig.key];
|
aValue = a[sortConfig.key];
|
||||||
bValue = b[sortConfig.key];
|
bValue = b[sortConfig.key];
|
||||||
@@ -227,11 +211,11 @@ const PlayerStatsTable = ({ playerStats }: PlayerStatsTableProps) => {
|
|||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
});
|
});
|
||||||
}, [playerStats, search, sortConfig]);
|
}, [statsWithMMR, search, sortConfig]);
|
||||||
|
|
||||||
const handlePlayerClick = (playerId: string) => {
|
const handlePlayerClick = useCallback((playerId: string) => {
|
||||||
navigate({ to: `/profile/${playerId}` });
|
navigate({ to: `/profile/${playerId}` });
|
||||||
};
|
}, [navigate]);
|
||||||
|
|
||||||
const handleSort = (key: SortKey) => {
|
const handleSort = (key: SortKey) => {
|
||||||
setSortConfig((prev) => ({
|
setSortConfig((prev) => ({
|
||||||
@@ -351,8 +335,8 @@ const PlayerStatsTable = ({ playerStats }: PlayerStatsTableProps) => {
|
|||||||
<Box key={stat.id}>
|
<Box key={stat.id}>
|
||||||
<PlayerListItem
|
<PlayerListItem
|
||||||
stat={stat}
|
stat={stat}
|
||||||
index={index}
|
|
||||||
onPlayerClick={handlePlayerClick}
|
onPlayerClick={handlePlayerClick}
|
||||||
|
mmr={stat.mmr}
|
||||||
/>
|
/>
|
||||||
{index < filteredAndSortedStats.length - 1 && <Divider />}
|
{index < filteredAndSortedStats.length - 1 && <Divider />}
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import Header from "./header";
|
|||||||
import SwipeableTabs from "@/components/swipeable-tabs";
|
import SwipeableTabs from "@/components/swipeable-tabs";
|
||||||
import { usePlayer, usePlayerMatches, usePlayerStats } from "../../queries";
|
import { usePlayer, usePlayerMatches, usePlayerStats } from "../../queries";
|
||||||
import TeamList from "@/features/teams/components/team-list";
|
import TeamList from "@/features/teams/components/team-list";
|
||||||
import StatsOverview from "@/shared/components/stats-overview";
|
import StatsOverview from "@/components/stats-overview";
|
||||||
import MatchList from "@/features/matches/components/match-list";
|
import MatchList from "@/features/matches/components/match-list";
|
||||||
|
|
||||||
interface ProfileProps {
|
interface ProfileProps {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import Header from "./header";
|
|||||||
import SwipeableTabs from "@/components/swipeable-tabs";
|
import SwipeableTabs from "@/components/swipeable-tabs";
|
||||||
import { usePlayer, usePlayerMatches, usePlayerStats } from "../../queries";
|
import { usePlayer, usePlayerMatches, usePlayerStats } from "../../queries";
|
||||||
import TeamList from "@/features/teams/components/team-list";
|
import TeamList from "@/features/teams/components/team-list";
|
||||||
import StatsOverview from "@/shared/components/stats-overview";
|
import StatsOverview, { StatsSkeleton } from "@/components/stats-overview";
|
||||||
import MatchList from "@/features/matches/components/match-list";
|
import MatchList from "@/features/matches/components/match-list";
|
||||||
import HeaderSkeleton from "./header-skeleton";
|
import HeaderSkeleton from "./header-skeleton";
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ const ProfileSkeleton = () => {
|
|||||||
const tabs = [
|
const tabs = [
|
||||||
{
|
{
|
||||||
label: "Overview",
|
label: "Overview",
|
||||||
content: <SkeletonLoader />,
|
content: <StatsSkeleton />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Matches",
|
label: "Matches",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Box, Divider, Text, Stack } from "@mantine/core";
|
|||||||
import Header from "./header";
|
import Header from "./header";
|
||||||
import SwipeableTabs from "@/components/swipeable-tabs";
|
import SwipeableTabs from "@/components/swipeable-tabs";
|
||||||
import TournamentList from "@/features/tournaments/components/tournament-list";
|
import TournamentList from "@/features/tournaments/components/tournament-list";
|
||||||
import StatsOverview from "@/shared/components/stats-overview";
|
import StatsOverview from "@/components/stats-overview";
|
||||||
import { useTeam, useTeamMatches, useTeamStats } from "../../queries";
|
import { useTeam, useTeamMatches, useTeamStats } from "../../queries";
|
||||||
import MatchList from "@/features/matches/components/match-list";
|
import MatchList from "@/features/matches/components/match-list";
|
||||||
import PlayerList from "@/features/players/components/player-list";
|
import PlayerList from "@/features/players/components/player-list";
|
||||||
|
|||||||
Reference in New Issue
Block a user