Files
flxn-app/src/features/players/components/player-head-to-head-sheet.tsx
2025-10-13 14:18:54 -05:00

280 lines
7.8 KiB
TypeScript

import { Stack, Text, Group, Box, Divider, Paper } from "@mantine/core";
import { usePlayerHeadToHead } from "@/features/matches/queries";
import { useMemo, useEffect, useState, Suspense } from "react";
import { CrownIcon } from "@phosphor-icons/react";
import MatchList from "@/features/matches/components/match-list";
import PlayerHeadToHeadSkeleton from "./player-head-to-head-skeleton";
interface PlayerHeadToHeadSheetProps {
player1Id: string;
player1Name: string;
player2Id: string;
player2Name: string;
isOpen?: boolean;
}
const PlayerHeadToHeadContent = ({
player1Id,
player1Name,
player2Id,
player2Name,
isOpen = true,
}: PlayerHeadToHeadSheetProps) => {
const [shouldFetch, setShouldFetch] = useState(false);
useEffect(() => {
if (isOpen && !shouldFetch) {
setShouldFetch(true);
}
}, [isOpen, shouldFetch]);
const { data: matches, isLoading } = usePlayerHeadToHead(player1Id, player2Id, shouldFetch);
const stats = useMemo(() => {
if (!matches || matches.length === 0) {
return {
player1Wins: 0,
player2Wins: 0,
player1CupsFor: 0,
player2CupsFor: 0,
player1CupsAgainst: 0,
player2CupsAgainst: 0,
player1AvgMargin: 0,
player2AvgMargin: 0,
};
}
let player1Wins = 0;
let player2Wins = 0;
let player1CupsFor = 0;
let player2CupsFor = 0;
let player1CupsAgainst = 0;
let player2CupsAgainst = 0;
let player1TotalWinMargin = 0;
let player2TotalWinMargin = 0;
matches.forEach((match) => {
const isPlayer1Home = match.home?.players?.some((p) => p.id === player1Id);
const player1Cups = isPlayer1Home ? match.home_cups : match.away_cups;
const player2Cups = isPlayer1Home ? match.away_cups : match.home_cups;
if (player1Cups > player2Cups) {
player1Wins++;
player1TotalWinMargin += (player1Cups - player2Cups);
} else if (player2Cups > player1Cups) {
player2Wins++;
player2TotalWinMargin += (player2Cups - player1Cups);
}
player1CupsFor += player1Cups;
player2CupsFor += player2Cups;
player1CupsAgainst += player2Cups;
player2CupsAgainst += player1Cups;
});
const player1AvgMargin =
player1Wins > 0 ? player1TotalWinMargin / player1Wins : 0;
const player2AvgMargin =
player2Wins > 0 ? player2TotalWinMargin / player2Wins : 0;
return {
player1Wins,
player2Wins,
player1CupsFor,
player2CupsFor,
player1CupsAgainst,
player2CupsAgainst,
player1AvgMargin,
player2AvgMargin,
};
}, [matches, player1Id]);
if (isLoading) {
return (
<Stack p="md" gap="md">
<Text size="sm" c="dimmed" ta="center">
Loading...
</Text>
</Stack>
);
}
if (!matches || matches.length === 0) {
return (
<Stack p="md" gap="md">
<Text size="sm" c="dimmed" ta="center">
These players have not faced each other yet.
</Text>
</Stack>
);
}
const totalMatches = stats.player1Wins + stats.player2Wins;
const leader =
stats.player1Wins > stats.player2Wins
? player1Name
: stats.player2Wins > stats.player1Wins
? player2Name
: null;
return (
<Stack gap="md">
<Paper p="md" withBorder radius="md">
<Stack gap="sm">
<Group justify="center" gap="xs">
<Text size="lg" fw={700}>
{player1Name}
</Text>
<Text size="sm" c="dimmed">
vs
</Text>
<Text size="lg" fw={700}>
{player2Name}
</Text>
</Group>
<Group justify="center" gap="lg">
<Stack gap={0} align="center">
<Text size="xl" fw={700}>
{stats.player1Wins}
</Text>
<Text size="xs" c="dimmed">
{player1Name}
</Text>
</Stack>
<Text size="md" c="dimmed">
-
</Text>
<Stack gap={0} align="center">
<Text size="xl" fw={700}>
{stats.player2Wins}
</Text>
<Text size="xs" c="dimmed">
{player2Name}
</Text>
</Stack>
</Group>
{leader && (
<Group justify="center" gap="xs">
<CrownIcon size={16} weight="fill" color="gold" />
<Text size="xs" c="dimmed">
{leader} leads the series
</Text>
</Group>
)}
{!leader && totalMatches > 0 && (
<Text size="xs" c="dimmed" ta="center">
Series is tied
</Text>
)}
</Stack>
</Paper>
<Stack gap={0}>
<Text size="sm" fw={600} px="md" mb="xs">
Stats Comparison
</Text>
<Paper withBorder>
<Stack gap={0}>
<Group justify="space-between" px="md" py="sm">
<Group gap="xs">
<Text size="sm" fw={600}>
{stats.player1CupsFor}
</Text>
<Text size="xs" c="dimmed">
cups
</Text>
</Group>
<Text size="xs" fw={500}>
Total Cups
</Text>
<Group gap="xs">
<Text size="xs" c="dimmed">
cups
</Text>
<Text size="sm" fw={600}>
{stats.player2CupsFor}
</Text>
</Group>
</Group>
<Divider />
<Group justify="space-between" px="md" py="sm">
<Group gap="xs">
<Text size="sm" fw={600}>
{totalMatches > 0
? (stats.player1CupsFor / totalMatches).toFixed(1)
: "0.0"}
</Text>
<Text size="xs" c="dimmed">
avg
</Text>
</Group>
<Text size="xs" fw={500}>
Avg Cups/Match
</Text>
<Group gap="xs">
<Text size="xs" c="dimmed">
avg
</Text>
<Text size="sm" fw={600}>
{totalMatches > 0
? (stats.player2CupsFor / totalMatches).toFixed(1)
: "0.0"}
</Text>
</Group>
</Group>
<Divider />
<Group justify="space-between" px="md" py="sm">
<Group gap="xs">
<Text size="sm" fw={600}>
{!isNaN(stats.player1AvgMargin)
? stats.player1AvgMargin.toFixed(1)
: "0.0"}
</Text>
<Text size="xs" c="dimmed">
margin
</Text>
</Group>
<Text size="xs" fw={500}>
Avg Win Margin
</Text>
<Group gap="xs">
<Text size="xs" c="dimmed">
margin
</Text>
<Text size="sm" fw={600}>
{!isNaN(stats.player2AvgMargin)
? stats.player2AvgMargin.toFixed(1)
: "0.0"}
</Text>
</Group>
</Group>
</Stack>
</Paper>
</Stack>
<Stack gap="xs">
<Text size="sm" fw={600} px="md">
Match History ({totalMatches})
</Text>
<MatchList matches={matches} hideH2H />
</Stack>
</Stack>
);
};
const PlayerHeadToHeadSheet = (props: PlayerHeadToHeadSheetProps) => {
return (
<Suspense fallback={<PlayerHeadToHeadSkeleton />}>
<PlayerHeadToHeadContent {...props} />
</Suspense>
);
};
export default PlayerHeadToHeadSheet;