From 3fe92be980a57437983b159d7ad0520b0661dcea Mon Sep 17 00:00:00 2001 From: yohlo Date: Sat, 13 Sep 2025 11:21:05 -0500 Subject: [PATCH] player stats in profile --- src/features/bracket/components/bracket.tsx | 26 +-- .../players/components/profile/index.tsx | 3 +- .../players/components/stats-overview.tsx | 187 ++++++++++++++++++ src/features/players/queries.ts | 12 +- src/features/players/server.ts | 9 +- src/features/players/types.ts | 15 ++ src/lib/pocketbase/services/players.ts | 8 + 7 files changed, 243 insertions(+), 17 deletions(-) create mode 100644 src/features/players/components/stats-overview.tsx diff --git a/src/features/bracket/components/bracket.tsx b/src/features/bracket/components/bracket.tsx index 5092b07..f0477eb 100644 --- a/src/features/bracket/components/bracket.tsx +++ b/src/features/bracket/components/bracket.tsx @@ -25,19 +25,19 @@ export const Bracket: React.FC = ({ justify="space-around" p={24} > - {round - .filter((match) => !match.bye) - .map((match) => { - return ( -
- -
- ); - })} + {round.map((match) => + match.bye ? ( +
+ ) : ( +
+ +
+ ) + )} ))} diff --git a/src/features/players/components/profile/index.tsx b/src/features/players/components/profile/index.tsx index 761c020..2cd9e5d 100644 --- a/src/features/players/components/profile/index.tsx +++ b/src/features/players/components/profile/index.tsx @@ -4,6 +4,7 @@ import { Player } from "@/features/players/types"; import SwipeableTabs from "@/components/swipeable-tabs"; import { usePlayer } from "../../queries"; import TeamList from "@/features/teams/components/team-list"; +import StatsOverview from "../stats-overview"; interface ProfileProps { id: string; @@ -14,7 +15,7 @@ const Profile = ({ id }: ProfileProps) => { const tabs = [ { label: "Overview", - content: Stats/Badges will go here, + content: , }, { label: "Matches", diff --git a/src/features/players/components/stats-overview.tsx b/src/features/players/components/stats-overview.tsx new file mode 100644 index 0000000..41ccdc6 --- /dev/null +++ b/src/features/players/components/stats-overview.tsx @@ -0,0 +1,187 @@ +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 { usePlayerStats } from "../queries"; + +interface StatsOverviewProps { + playerId: string; +} + +const StatCard = ({ + label, + value, + suffix = "", + Icon +}: { + label: string; + value: number | null; + suffix?: string; + Icon?: Icon; +}) => ( + + +
+ + {label} + + + {value !== null ? `${value}${suffix}` : "—"} + +
+ {Icon && ( + + + + )} +
+
+); + +const StatsOverview = ({ playerId }: StatsOverviewProps) => { + const { data: statsData } = usePlayerStats(playerId); + + if (!statsData || statsData.length === 0) { + return ( + +
+ + No stats available yet + +
+
+ ); + } + + // Aggregate stats across all teams + const overallStats = statsData.reduce( + (acc, stat) => ({ + matches: acc.matches + stat.matches, + wins: acc.wins + stat.wins, + losses: acc.losses + stat.losses, + total_cups_made: acc.total_cups_made + stat.total_cups_made, + total_cups_against: acc.total_cups_against + stat.total_cups_against, + }), + { matches: 0, wins: 0, losses: 0, total_cups_made: 0, total_cups_against: 0 } + ); + + const winPercentage = overallStats.matches > 0 + ? ((overallStats.wins / overallStats.matches) * 100) + : 0; + + const avgCupsPerMatch = overallStats.matches > 0 + ? (overallStats.total_cups_made / overallStats.matches) + : 0; + + const avgCupsAgainstPerMatch = overallStats.matches > 0 + ? (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); + + const avgMarginOfVictory = validMarginOfVictory.length > 0 + ? (validMarginOfVictory.reduce((acc, stat) => acc + stat.margin_of_victory, 0) / validMarginOfVictory.length) + : 0; + + const avgMarginOfLoss = validMarginOfLoss.length > 0 + ? (validMarginOfLoss.reduce((acc, stat) => acc + stat.margin_of_loss, 0) / validMarginOfLoss.length) + : 0; + + return ( + + + Stats + + + + + + + + + + + + + + + + + + + + + + + + + + + + {/* Team Breakdown */} + {statsData.length > 1 && ( +
+ Teams + + {statsData.map((stat) => ( +
+ + + + {stat.player_name.split(' ').map(n => n[0]).join('')} + +
+ {stat.player_name} + + {stat.matches}M • {stat.wins}W - {stat.losses}L + +
+
+ + {((stat.wins / stat.matches) * 100).toFixed(0)}% + +
+
+ ))} +
+
+ )} +
+
+ ); +}; + +export default StatsOverview; \ No newline at end of file diff --git a/src/features/players/queries.ts b/src/features/players/queries.ts index c73a930..422a9e2 100644 --- a/src/features/players/queries.ts +++ b/src/features/players/queries.ts @@ -1,11 +1,12 @@ import { useServerSuspenseQuery } from "@/lib/tanstack-query/hooks"; -import { listPlayers, getPlayer, getUnassociatedPlayers, fetchMe } from "./server"; +import { listPlayers, getPlayer, getUnassociatedPlayers, fetchMe, getPlayerStats } from "./server"; export const playerKeys = { auth: ['auth'], list: ['players', 'list'], details: (id: string) => ['players', 'details', id], unassociated: ['players','unassociated'], + stats: (id: string) => ['players', 'stats', id], }; export const playerQueries = { @@ -25,6 +26,10 @@ export const playerQueries = { queryKey: playerKeys.unassociated, queryFn: async () => await getUnassociatedPlayers() }), + stats: (id: string) => ({ + queryKey: playerKeys.stats(id), + queryFn: async () => await getPlayerStats({ data: id }) + }), }; export const useMe = () => { @@ -57,4 +62,7 @@ export const usePlayers = () => useServerSuspenseQuery(playerQueries.list()); export const useUnassociatedPlayers = () => - useServerSuspenseQuery(playerQueries.unassociated()); \ No newline at end of file + useServerSuspenseQuery(playerQueries.unassociated()); + +export const usePlayerStats = (id: string) => + useServerSuspenseQuery(playerQueries.stats(id)); \ No newline at end of file diff --git a/src/features/players/server.ts b/src/features/players/server.ts index 7edb775..cd29c87 100644 --- a/src/features/players/server.ts +++ b/src/features/players/server.ts @@ -1,6 +1,6 @@ import { setUserMetadata, superTokensFunctionMiddleware, getSessionContext } from "@/utils/supertokens"; import { createServerFn } from "@tanstack/react-start"; -import { Player, playerInputSchema, playerUpdateSchema } from "@/features/players/types"; +import { Player, playerInputSchema, playerUpdateSchema, PlayerStats } from "@/features/players/types"; import { pbAdmin } from "@/lib/pocketbase/client"; import { z } from "zod"; import { logger } from "."; @@ -118,4 +118,11 @@ export const getUnassociatedPlayers = createServerFn() .middleware([superTokensFunctionMiddleware]) .handler(async () => toServerResult(pbAdmin.getUnassociatedPlayers) + ); + +export const getPlayerStats = createServerFn() + .validator(z.string()) + .middleware([superTokensFunctionMiddleware]) + .handler(async ({ data }) => + toServerResult(async () => await pbAdmin.getPlayerStats(data)) ); \ No newline at end of file diff --git a/src/features/players/types.ts b/src/features/players/types.ts index 0504b6b..647049b 100644 --- a/src/features/players/types.ts +++ b/src/features/players/types.ts @@ -27,3 +27,18 @@ export const playerUpdateSchema = playerInputSchema.partial(); export type PlayerInput = z.infer; export type PlayerUpdateInput = z.infer; + +export interface PlayerStats { + id: string; + player_id: string; + player_name: string; + matches: number; + wins: number; + losses: number; + total_cups_made: number; + total_cups_against: number; + win_percentage: number; + avg_cups_per_match: number; + margin_of_victory: number; + margin_of_loss: number; +} diff --git a/src/lib/pocketbase/services/players.ts b/src/lib/pocketbase/services/players.ts index 9f4f7ba..17d1e27 100644 --- a/src/lib/pocketbase/services/players.ts +++ b/src/lib/pocketbase/services/players.ts @@ -3,6 +3,7 @@ import type { PlayerInfo, PlayerInput, PlayerUpdateInput, + PlayerStats, } from "@/features/players/types"; import { transformPlayer, transformPlayerInfo } from "@/lib/pocketbase/util/transform-types"; import PocketBase from "pocketbase"; @@ -62,5 +63,12 @@ export function createPlayersService(pb: PocketBase) { }); return result.map(transformPlayer); }, + + async getPlayerStats(playerId: string): Promise { + const result = await pb.collection("player_stats").getFullList({ + filter: `player_id = "${playerId}"`, + }); + return result; + }, }; }