193 lines
6.7 KiB
TypeScript
193 lines
6.7 KiB
TypeScript
import { useMemo, memo } from "react";
|
|
import {
|
|
Stack,
|
|
Text,
|
|
Group,
|
|
UnstyledButton,
|
|
Container,
|
|
Box,
|
|
Center,
|
|
ThemeIcon,
|
|
Divider,
|
|
Alert,
|
|
} from "@mantine/core";
|
|
import { Tournament } from "@/features/tournaments/types";
|
|
import { CrownIcon, TreeStructureIcon, InfoIcon, ListDashes } from "@phosphor-icons/react";
|
|
import TeamAvatar from "@/components/team-avatar";
|
|
import ListLink from "@/components/list-link";
|
|
import { Podium } from "./podium";
|
|
|
|
interface TournamentStatsProps {
|
|
tournament: Tournament;
|
|
}
|
|
|
|
export const TournamentStats = memo(({ tournament }: TournamentStatsProps) => {
|
|
|
|
const matches = tournament.matches || [];
|
|
const nonByeMatches = useMemo(() =>
|
|
matches.filter((match) => !(match.status === 'tbd' && match.bye === true)),
|
|
[matches]
|
|
);
|
|
const isComplete = useMemo(() =>
|
|
nonByeMatches.length > 0 && nonByeMatches.every((match) => match.status === 'ended'),
|
|
[nonByeMatches]
|
|
);
|
|
|
|
const hasGroupStage = useMemo(() => {
|
|
return tournament.matches?.some((match) => match.round === -1) || false;
|
|
}, [tournament.matches]);
|
|
|
|
const sortedTeamStats = useMemo(() => {
|
|
return [...(tournament.team_stats || [])].sort((a, b) => {
|
|
if (b.wins !== a.wins) {
|
|
return b.wins - a.wins;
|
|
}
|
|
return b.total_cups_made - a.total_cups_made;
|
|
});
|
|
}, [tournament.team_stats]);
|
|
|
|
const teamStatsWithCalculations = useMemo(() => {
|
|
return sortedTeamStats.map((stat) => ({
|
|
...stat,
|
|
winPercentage: stat.matches > 0 ? (stat.wins / stat.matches) * 100 : 0,
|
|
avgCupsPerMatch: stat.matches > 0 ? stat.total_cups_made / stat.matches : 0,
|
|
})).sort((a, b) => b.winPercentage - a.winPercentage);;
|
|
}, [sortedTeamStats]);
|
|
|
|
const renderTeamStatsTable = () => {
|
|
if (!teamStatsWithCalculations.length) {
|
|
return (
|
|
<Box p="md">
|
|
<Center>
|
|
<Text c="dimmed" size="sm">
|
|
No stats available yet
|
|
</Text>
|
|
</Center>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Stack gap={0}>
|
|
<Text px="md" size="lg" fw={600}>Results</Text>
|
|
<Text px="md" c="dimmed" size="xs" fw={500}>Sorted by win percentage</Text>
|
|
{teamStatsWithCalculations.map((stat, index) => {
|
|
const team = tournament.teams?.find(t => t.id === stat.team_id);
|
|
|
|
return (
|
|
<Box key={stat.id}>
|
|
<UnstyledButton
|
|
w="100%"
|
|
p="md"
|
|
style={{ borderRadius: 0 }}
|
|
>
|
|
<Group justify="space-between" align="center" w="100%">
|
|
<Group gap="sm" align="center">
|
|
{team ? (
|
|
<TeamAvatar team={team} size={40} radius="sm" isRegional={tournament.regional} />
|
|
) : (
|
|
<TeamAvatar team={{ id: stat.team_id, name: stat.team_name, players: [] } as any} size={40} radius="sm" isRegional={tournament.regional} />
|
|
)}
|
|
<Stack gap={2}>
|
|
<Group gap='xs'>
|
|
<Text size="xs" c="dimmed">
|
|
#{index + 1}
|
|
</Text>
|
|
<Text size="sm" fw={600}>
|
|
{stat.team_name}
|
|
</Text>
|
|
{index === 0 && isComplete && (
|
|
<ThemeIcon size="xs" color="yellow" variant="light" radius="xl">
|
|
<CrownIcon size={12} />
|
|
</ThemeIcon>
|
|
)}
|
|
</Group>
|
|
<Group gap="md" ta="center">
|
|
<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.winPercentage.toFixed(1)}%
|
|
</Text>
|
|
</Stack>
|
|
<Stack gap={0}>
|
|
<Text size="xs" c="dimmed" fw={700}>
|
|
AVG
|
|
</Text>
|
|
<Text size="xs" c="dimmed">
|
|
{stat.avgCupsPerMatch.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>
|
|
</Group>
|
|
</UnstyledButton>
|
|
{index < teamStatsWithCalculations.length - 1 && <Divider />}
|
|
</Box>
|
|
);
|
|
})}
|
|
</Stack>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<Container size="100%" px={0}>
|
|
<Stack gap="md">
|
|
{tournament.regional && !hasGroupStage && (
|
|
<Alert px="md" variant="light" title="Regional Tournament" icon={<InfoIcon size={16} />}>
|
|
Earlier regional formats aren't supported in the app and order of matches or displayed winners may be unreliable.
|
|
</Alert>
|
|
)}
|
|
{!tournament.regional && <Podium tournament={tournament} />}
|
|
{hasGroupStage && (
|
|
<ListLink
|
|
label={`View Groups`}
|
|
to={`/tournaments/${tournament.id}/groups`}
|
|
Icon={ListDashes}
|
|
/>
|
|
)}
|
|
<ListLink
|
|
label={`View Bracket`}
|
|
to={`/tournaments/${tournament.id}/bracket`}
|
|
Icon={TreeStructureIcon}
|
|
/>
|
|
{renderTeamStatsTable()}
|
|
</Stack>
|
|
</Container>
|
|
);
|
|
});
|