skeletons

This commit is contained in:
yohlo
2025-09-22 16:45:41 -05:00
parent fc3f626313
commit cae5fa1c71
10 changed files with 183 additions and 58 deletions

View 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)
})

View 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)
})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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