redesign stats

This commit is contained in:
yohlo
2025-09-13 11:36:30 -05:00
parent 3fe92be980
commit 617a94262b
2 changed files with 230 additions and 117 deletions

View File

@@ -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: () => {

View File

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