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: settingsLink:
context?.auth.user.id === params.playerId ? "/settings" : undefined, context?.auth.user.id === params.playerId ? "/settings" : undefined,
}, },
withPadding: false,
refresh: [playerQueries.details(params.playerId).queryKey], refresh: [playerQueries.details(params.playerId).queryKey],
}), }),
component: () => { component: () => {

View File

@@ -1,40 +1,96 @@
import { Box, Grid, Text, Group, Stack, ThemeIcon, Card, Avatar } from "@mantine/core"; import { Box, Grid, Text, Group, Stack, ThemeIcon, Card, Avatar, Progress, Badge, Divider } from "@mantine/core";
import { TrophyIcon, CrownIcon, XIcon, FireIcon, ShieldIcon, ChartLineUpIcon, ShieldCheckIcon, BoxingGloveIcon, Icon } from "@phosphor-icons/react"; import { CrownIcon, XIcon, FireIcon, ShieldIcon, ChartLineUpIcon, ShieldCheckIcon, BoxingGloveIcon, Icon, TrendUpIcon, ArrowUpIcon, ArrowDownIcon } from "@phosphor-icons/react";
import { usePlayerStats } from "../queries"; import { usePlayerStats } from "../queries";
interface StatsOverviewProps { interface StatsOverviewProps {
playerId: string; playerId: string;
} }
const StatCard = ({ const StatCard = ({
label, label,
value, value,
suffix = "", suffix = "",
Icon Icon,
}: { variant = "default"
label: string; }: {
value: number | null; label: string;
value: number | null;
suffix?: string; suffix?: string;
Icon?: Icon; Icon?: Icon;
}) => ( variant?: "default" | "featured" | "compact";
<Card p='xs'> }) => {
<Group justify="space-between" align="center" gap="xs"> if (variant === "featured") {
<div> return (
<Text size="xs" c="dimmed" fw={500} tt="uppercase"> <Card
{label} p="lg"
</Text> radius="lg"
<Text size="sm" fw={600}> 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}` : "—"} {value !== null ? `${value}${suffix}` : "—"}
</Text> </Text>
</div> </Stack>
{Icon && ( </Card>
<ThemeIcon size="sm" variant="subtle" c="blue"> );
<Icon size={14} /> };
</ThemeIcon>
)}
</Group>
</Card>
);
const StatsOverview = ({ playerId }: StatsOverviewProps) => { const StatsOverview = ({ playerId }: StatsOverviewProps) => {
const { data: statsData } = usePlayerStats(playerId); const { data: statsData } = usePlayerStats(playerId);
@@ -51,7 +107,6 @@ const StatsOverview = ({ playerId }: StatsOverviewProps) => {
); );
} }
// Aggregate stats across all teams
const overallStats = statsData.reduce( const overallStats = statsData.reduce(
(acc, stat) => ({ (acc, stat) => ({
matches: acc.matches + stat.matches, matches: acc.matches + stat.matches,
@@ -75,7 +130,6 @@ const StatsOverview = ({ playerId }: StatsOverviewProps) => {
? (overallStats.total_cups_against / overallStats.matches) ? (overallStats.total_cups_against / overallStats.matches)
: 0; : 0;
// Calculate average margins from individual team stats
const validMarginOfVictory = statsData.filter(stat => stat.margin_of_victory > 0); const validMarginOfVictory = statsData.filter(stat => stat.margin_of_victory > 0);
const validMarginOfLoss = statsData.filter(stat => stat.margin_of_loss > 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) ? (validMarginOfLoss.reduce((acc, stat) => acc + stat.margin_of_loss, 0) / validMarginOfLoss.length)
: 0; : 0;
return ( const getWinRateColor = (rate: number) => {
<Box p="sm"> if (rate >= 70) return "green";
<Stack gap="xs"> if (rate >= 50) return "blue";
<Text size="md" fw={600} mb="xs">Stats</Text> if (rate >= 30) return "orange";
<Grid gutter="xs"> return "red";
<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>
{/* 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 && ( {statsData.length > 1 && (
<div> <>
<Text size="md" fw={600} mb="xs">Teams</Text> <Divider />
<Stack gap={4}> <Stack gap="sm">
{statsData.map((stat) => ( <Text size="md" fw={600} c="dark">Team Performance</Text>
<div key={stat.id} style={{ padding: '8px', border: '1px solid var(--mantine-color-gray-3)', borderRadius: '4px' }}> <Stack gap="xs">
<Group justify="space-between"> {statsData.map((stat) => {
<Group gap="xs"> const teamWinRate = (stat.wins / stat.matches) * 100;
<Avatar size="xs" color="blue"> return (
{stat.player_name.split(' ').map(n => n[0]).join('')} <Card key={stat.id} p="md" radius="md" withBorder>
</Avatar> <Group justify="space-between" align="center">
<div> <Group gap="sm">
<Text size="xs" fw={500}>{stat.player_name}</Text> <Avatar
<Text size="xs" c="dimmed"> size="md"
{stat.matches}M {stat.wins}W - {stat.losses}L color={getWinRateColor(teamWinRate)}
</Text> radius="lg"
</div> >
</Group> {stat.player_name.split(' ').map(n => n[0]).join('')}
<Text size="xs" fw={600}> </Avatar>
{((stat.wins / stat.matches) * 100).toFixed(0)}% <Stack gap={2}>
</Text> <Text size="sm" fw={600}>{stat.player_name}</Text>
</Group> <Group gap="xs">
</div> <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> </Stack>
</div> </>
)} )}
</Stack> </Stack>
</Box> </Box>