redesign stats
This commit is contained in:
@@ -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: () => {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user