redesign stats
This commit is contained in:
@@ -24,6 +24,7 @@ export const Route = createFileRoute("/_authed/profile/$playerId")({
|
||||
settingsLink:
|
||||
context?.auth.user.id === params.playerId ? "/settings" : undefined,
|
||||
},
|
||||
withPadding: false,
|
||||
refresh: [playerQueries.details(params.playerId).queryKey],
|
||||
}),
|
||||
component: () => {
|
||||
|
||||
@@ -1,40 +1,96 @@
|
||||
import { Box, Grid, Text, Group, Stack, ThemeIcon, Card, Avatar } from "@mantine/core";
|
||||
import { TrophyIcon, CrownIcon, XIcon, FireIcon, ShieldIcon, ChartLineUpIcon, ShieldCheckIcon, BoxingGloveIcon, Icon } from "@phosphor-icons/react";
|
||||
import { Box, Grid, Text, Group, Stack, ThemeIcon, Card, Avatar, Progress, Badge, Divider } from "@mantine/core";
|
||||
import { CrownIcon, XIcon, FireIcon, ShieldIcon, ChartLineUpIcon, ShieldCheckIcon, BoxingGloveIcon, Icon, TrendUpIcon, ArrowUpIcon, ArrowDownIcon } from "@phosphor-icons/react";
|
||||
import { usePlayerStats } from "../queries";
|
||||
|
||||
interface StatsOverviewProps {
|
||||
playerId: string;
|
||||
}
|
||||
|
||||
const StatCard = ({
|
||||
label,
|
||||
value,
|
||||
suffix = "",
|
||||
Icon
|
||||
}: {
|
||||
label: string;
|
||||
value: number | null;
|
||||
const StatCard = ({
|
||||
label,
|
||||
value,
|
||||
suffix = "",
|
||||
Icon,
|
||||
variant = "default"
|
||||
}: {
|
||||
label: string;
|
||||
value: number | null;
|
||||
suffix?: string;
|
||||
Icon?: Icon;
|
||||
}) => (
|
||||
<Card p='xs'>
|
||||
<Group justify="space-between" align="center" gap="xs">
|
||||
<div>
|
||||
<Text size="xs" c="dimmed" fw={500} tt="uppercase">
|
||||
{label}
|
||||
</Text>
|
||||
<Text size="sm" fw={600}>
|
||||
variant?: "default" | "featured" | "compact";
|
||||
}) => {
|
||||
if (variant === "featured") {
|
||||
return (
|
||||
<Card
|
||||
p="lg"
|
||||
radius="lg"
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, var(--mantine-color-blue-6) 0%, var(--mantine-color-blue-7) 100%)',
|
||||
color: 'white',
|
||||
border: 'none'
|
||||
}}
|
||||
>
|
||||
<Stack gap="xs" align="center" ta="center">
|
||||
{Icon && (
|
||||
<ThemeIcon size="lg" variant="white" color="blue" radius="lg">
|
||||
<Icon size={24} />
|
||||
</ThemeIcon>
|
||||
)}
|
||||
<div>
|
||||
<Text size="xl" fw={700} lh={1}>
|
||||
{value !== null ? `${value}${suffix}` : "—"}
|
||||
</Text>
|
||||
<Text size="sm" opacity={0.9} fw={500} tt="uppercase" lts="0.5px">
|
||||
{label}
|
||||
</Text>
|
||||
</div>
|
||||
</Stack>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
if (variant === "compact") {
|
||||
return (
|
||||
<Card p="sm" radius="md" withBorder>
|
||||
<Group justify="space-between" align="flex-start">
|
||||
<Stack gap={2} flex={1}>
|
||||
<Text size="xs" c="dimmed" fw={600} tt="uppercase" lts="0.3px">
|
||||
{label}
|
||||
</Text>
|
||||
<Text size="md" fw={700} lh={1}>
|
||||
{value !== null ? `${value}${suffix}` : "—"}
|
||||
</Text>
|
||||
</Stack>
|
||||
{Icon && (
|
||||
<ThemeIcon size="md" variant="light" radius="md">
|
||||
<Icon size={16} />
|
||||
</ThemeIcon>
|
||||
)}
|
||||
</Group>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card p="md" radius="md" withBorder>
|
||||
<Stack gap="xs">
|
||||
<Group justify="space-between" align="center">
|
||||
<Text size="xs" c="dimmed" fw={600} tt="uppercase" lts="0.3px">
|
||||
{label}
|
||||
</Text>
|
||||
{Icon && (
|
||||
<ThemeIcon size="sm" variant="light" radius="sm">
|
||||
<Icon size={14} />
|
||||
</ThemeIcon>
|
||||
)}
|
||||
</Group>
|
||||
<Text size="xl" fw={700} lh={1}>
|
||||
{value !== null ? `${value}${suffix}` : "—"}
|
||||
</Text>
|
||||
</div>
|
||||
{Icon && (
|
||||
<ThemeIcon size="sm" variant="subtle" c="blue">
|
||||
<Icon size={14} />
|
||||
</ThemeIcon>
|
||||
)}
|
||||
</Group>
|
||||
</Card>
|
||||
);
|
||||
</Stack>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
const StatsOverview = ({ playerId }: StatsOverviewProps) => {
|
||||
const { data: statsData } = usePlayerStats(playerId);
|
||||
@@ -51,7 +107,6 @@ const StatsOverview = ({ playerId }: StatsOverviewProps) => {
|
||||
);
|
||||
}
|
||||
|
||||
// Aggregate stats across all teams
|
||||
const overallStats = statsData.reduce(
|
||||
(acc, stat) => ({
|
||||
matches: acc.matches + stat.matches,
|
||||
@@ -75,7 +130,6 @@ const StatsOverview = ({ playerId }: StatsOverviewProps) => {
|
||||
? (overallStats.total_cups_against / overallStats.matches)
|
||||
: 0;
|
||||
|
||||
// Calculate average margins from individual team stats
|
||||
const validMarginOfVictory = statsData.filter(stat => stat.margin_of_victory > 0);
|
||||
const validMarginOfLoss = statsData.filter(stat => stat.margin_of_loss > 0);
|
||||
|
||||
@@ -87,97 +141,155 @@ const StatsOverview = ({ playerId }: StatsOverviewProps) => {
|
||||
? (validMarginOfLoss.reduce((acc, stat) => acc + stat.margin_of_loss, 0) / validMarginOfLoss.length)
|
||||
: 0;
|
||||
|
||||
return (
|
||||
<Box p="sm">
|
||||
<Stack gap="xs">
|
||||
<Text size="md" fw={600} mb="xs">Stats</Text>
|
||||
<Grid gutter="xs">
|
||||
<Grid.Col span={6}>
|
||||
<StatCard
|
||||
label="Matches"
|
||||
value={overallStats.matches}
|
||||
Icon={BoxingGloveIcon}
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={6}>
|
||||
<StatCard
|
||||
label="Win Rate"
|
||||
value={parseFloat(winPercentage.toFixed(1))}
|
||||
suffix="%"
|
||||
Icon={TrophyIcon}
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={6}>
|
||||
<StatCard
|
||||
label="Wins"
|
||||
value={overallStats.wins}
|
||||
Icon={CrownIcon}
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={6}>
|
||||
<StatCard
|
||||
label="Losses"
|
||||
value={overallStats.losses}
|
||||
Icon={XIcon}
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={6}>
|
||||
<StatCard
|
||||
label="Cups Made"
|
||||
value={overallStats.total_cups_made}
|
||||
Icon={FireIcon}
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={6}>
|
||||
<StatCard
|
||||
label="Cups Against"
|
||||
value={overallStats.total_cups_against}
|
||||
Icon={ShieldIcon}
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={6}>
|
||||
<StatCard
|
||||
label="Avg Per Game"
|
||||
value={parseFloat(avgCupsPerMatch.toFixed(1))}
|
||||
Icon={ChartLineUpIcon}
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={6}>
|
||||
<StatCard
|
||||
label="Avg Against"
|
||||
value={parseFloat(avgCupsAgainstPerMatch.toFixed(1))}
|
||||
Icon={ShieldCheckIcon}
|
||||
/>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
const getWinRateColor = (rate: number) => {
|
||||
if (rate >= 70) return "green";
|
||||
if (rate >= 50) return "blue";
|
||||
if (rate >= 30) return "orange";
|
||||
return "red";
|
||||
};
|
||||
|
||||
{/* Team Breakdown */}
|
||||
return (
|
||||
<Box p="md">
|
||||
<Stack gap="lg">
|
||||
|
||||
<Divider />
|
||||
|
||||
<Stack gap="sm">
|
||||
<Text size="md" fw={600} c="dark">Match Statistics</Text>
|
||||
<Grid gutter="md">
|
||||
<Grid.Col span={4}>
|
||||
<StatCard
|
||||
label="Matches"
|
||||
value={overallStats.matches}
|
||||
Icon={BoxingGloveIcon}
|
||||
variant="compact"
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={4}>
|
||||
<StatCard
|
||||
label="Wins"
|
||||
value={overallStats.wins}
|
||||
Icon={CrownIcon}
|
||||
variant="compact"
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={4}>
|
||||
<StatCard
|
||||
label="Losses"
|
||||
value={overallStats.losses}
|
||||
Icon={XIcon}
|
||||
variant="compact"
|
||||
/>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</Stack>
|
||||
|
||||
<Stack gap="sm">
|
||||
<Text size="md" fw={600} c="dark">Metrics</Text>
|
||||
<Grid gutter="md">
|
||||
<Grid.Col span={6}>
|
||||
<StatCard
|
||||
label="Cups Made"
|
||||
value={overallStats.total_cups_made}
|
||||
Icon={FireIcon}
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={6}>
|
||||
<StatCard
|
||||
label="Cups Against"
|
||||
value={overallStats.total_cups_against}
|
||||
Icon={ShieldIcon}
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={6}>
|
||||
<StatCard
|
||||
label="Avg Per Game"
|
||||
value={parseFloat(avgCupsPerMatch.toFixed(1))}
|
||||
Icon={ChartLineUpIcon}
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={6}>
|
||||
<StatCard
|
||||
label="Avg Against"
|
||||
value={parseFloat(avgCupsAgainstPerMatch.toFixed(1))}
|
||||
Icon={ShieldCheckIcon}
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={6}>
|
||||
<StatCard
|
||||
label="Avg Win Margin"
|
||||
value={avgMarginOfVictory > 0 ? parseFloat(avgMarginOfVictory.toFixed(1)) : null}
|
||||
Icon={ArrowUpIcon}
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={6}>
|
||||
<StatCard
|
||||
label="Avg Loss Margin"
|
||||
value={avgMarginOfLoss > 0 ? parseFloat(avgMarginOfLoss.toFixed(1)) : null}
|
||||
Icon={ArrowDownIcon}
|
||||
/>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</Stack>
|
||||
|
||||
{/* Team Performance */}
|
||||
{statsData.length > 1 && (
|
||||
<div>
|
||||
<Text size="md" fw={600} mb="xs">Teams</Text>
|
||||
<Stack gap={4}>
|
||||
{statsData.map((stat) => (
|
||||
<div key={stat.id} style={{ padding: '8px', border: '1px solid var(--mantine-color-gray-3)', borderRadius: '4px' }}>
|
||||
<Group justify="space-between">
|
||||
<Group gap="xs">
|
||||
<Avatar size="xs" color="blue">
|
||||
{stat.player_name.split(' ').map(n => n[0]).join('')}
|
||||
</Avatar>
|
||||
<div>
|
||||
<Text size="xs" fw={500}>{stat.player_name}</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
{stat.matches}M • {stat.wins}W - {stat.losses}L
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
<Text size="xs" fw={600}>
|
||||
{((stat.wins / stat.matches) * 100).toFixed(0)}%
|
||||
</Text>
|
||||
</Group>
|
||||
</div>
|
||||
))}
|
||||
<>
|
||||
<Divider />
|
||||
<Stack gap="sm">
|
||||
<Text size="md" fw={600} c="dark">Team Performance</Text>
|
||||
<Stack gap="xs">
|
||||
{statsData.map((stat) => {
|
||||
const teamWinRate = (stat.wins / stat.matches) * 100;
|
||||
return (
|
||||
<Card key={stat.id} p="md" radius="md" withBorder>
|
||||
<Group justify="space-between" align="center">
|
||||
<Group gap="sm">
|
||||
<Avatar
|
||||
size="md"
|
||||
color={getWinRateColor(teamWinRate)}
|
||||
radius="lg"
|
||||
>
|
||||
{stat.player_name.split(' ').map(n => n[0]).join('')}
|
||||
</Avatar>
|
||||
<Stack gap={2}>
|
||||
<Text size="sm" fw={600}>{stat.player_name}</Text>
|
||||
<Group gap="xs">
|
||||
<Text size="xs" c="dimmed">
|
||||
{stat.matches} matches
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed">•</Text>
|
||||
<Text size="xs" c="green.6">
|
||||
{stat.wins}W
|
||||
</Text>
|
||||
<Text size="xs" c="red.6">
|
||||
{stat.losses}L
|
||||
</Text>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Group>
|
||||
<Stack gap={4} align="flex-end">
|
||||
<Badge
|
||||
size="sm"
|
||||
variant="light"
|
||||
color={getWinRateColor(teamWinRate)}
|
||||
>
|
||||
{teamWinRate.toFixed(0)}%
|
||||
</Badge>
|
||||
<Progress
|
||||
value={teamWinRate}
|
||||
size="xs"
|
||||
w={60}
|
||||
color={getWinRateColor(teamWinRate)}
|
||||
/>
|
||||
</Stack>
|
||||
</Group>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
Reference in New Issue
Block a user