Skip to content

Commit

Permalink
Merge pull request #69 from soham2k06/feat/player-team-stats
Browse files Browse the repository at this point in the history
batting stats
  • Loading branch information
soham2k06 authored Apr 17, 2024
2 parents c8c8b13 + ea95352 commit aca5586
Show file tree
Hide file tree
Showing 10 changed files with 292 additions and 15 deletions.
2 changes: 2 additions & 0 deletions apiHooks/player/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useCreatePlayer } from "./useCreatePlayer";
import { useDeletePlayer } from "./useDeletePlayer";
import { usePlayerById } from "./usePlayerById";
import { useUpdatePlayer } from "./useUpdatePlayer";
import { usePlayerStats } from "./usePlayerStats";

export {
useAllPlayers,
Expand All @@ -12,4 +13,5 @@ export {
usePlayerById,
useDeletePlayer,
useUpdatePlayer,
usePlayerStats,
};
17 changes: 17 additions & 0 deletions apiHooks/player/usePlayerStats.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { useQuery } from "@tanstack/react-query";
import { getPlayerStats } from "@/services/player/getPlayerStats";

export function usePlayerStats(id: string | undefined) {
const { data, isLoading, isFetching, error } = useQuery({
queryKey: ["playerStats", id],
queryFn: () => getPlayerStats(id),
enabled: !!id,
});

return {
data,
isLoading,
isFetching,
error,
};
}
96 changes: 96 additions & 0 deletions app/api/players/stats/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import prisma from "@/lib/db/prisma";
import { getScore, validateUser } from "@/lib/utils";
import { BallEvent } from "@prisma/client";
import { NextResponse } from "next/server";

export async function GET(
_: unknown,
{ params: { id } }: { params: { id: string } },
) {
try {
validateUser();

const playerBallEvents = await prisma.ballEvent.findMany({
where: { OR: [{ batsmanId: id }, { bowlerId: id }] },
});

const groupedMatches: { [matchId: string]: BallEvent[] } = {};

for (const event of playerBallEvents) {
const matchId = event.matchId ?? "no-data";
if (!groupedMatches[matchId]) {
groupedMatches[matchId] = [];
}
groupedMatches[matchId].push(event);
}

let fifties = 0;
let centuries = 0;
let highestScore = 0;
for (const matchId in groupedMatches) {
const matchEvents = groupedMatches[matchId];

const { runs } = getScore(
matchEvents.map((event) => event.type),
true,
);

if (runs >= 50 && runs < 100) fifties++;
if (runs >= 100) centuries++;
if (runs > highestScore) highestScore = runs;
}

const matchIds = playerBallEvents.map((event) => event.matchId);
const uniqueMatchIds = new Set(matchIds);
const matchesPlayed = uniqueMatchIds.size;

const battingEvents = playerBallEvents
.filter((event) => event.batsmanId === id)
.map((event) => event.type);

const bowlingEvents = playerBallEvents
.filter((event) => event.bowlerId === id)
.map((event) => event.type);

const {
runs: runsScored,
totalBalls: ballsFaced,
wickets: outs,
} = getScore(battingEvents, true);

const battingStats = {
runs: runsScored,
balls: ballsFaced,
wickets: outs,
};

const {
runs: runsConceded,
totalBalls: ballsBowled,
wickets: wicketsTaken,
} = getScore(bowlingEvents);

const bowlingStats = {
runs: runsConceded,
balls: ballsBowled,
wickets: wicketsTaken,
};

const playerStats = {
matchesPlayed,
batting: { ...battingStats, fifties, centuries, highestScore },
bowling: bowlingStats,
};

if (!playerStats)
return NextResponse.json({ error: "No data found" }, { status: 404 });

return NextResponse.json(playerStats, { status: 200 });
} catch (error) {
console.error(error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 },
);
}
}
43 changes: 35 additions & 8 deletions components/players/Player.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,39 @@
import { Player as PlayerSchemaType } from "@prisma/client";
import { Edit, MoreHorizontal, Trash2 } from "lucide-react";

import { truncStr } from "@/lib/utils";
import { UpdatePlayerSchema } from "@/lib/validation/player";

import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";

import { Card, CardHeader, CardTitle } from "../ui/card";
} from "../ui/dropdown-menu";
import { Card, CardContent, CardHeader, CardTitle } from "../ui/card";
import { Button } from "../ui/button";
import { Player as PlayerSchemaType } from "@prisma/client";
import { truncStr } from "@/lib/utils";
import { UpdatePlayerSchema } from "@/lib/validation/player";
import { Edit, MoreHorizontal, Trash2 } from "lucide-react";

interface PlayerProps {
player: PlayerSchemaType;
setPlayerToDelete: (playerId: string) => void;
setPlayerToUpdate: (player: UpdatePlayerSchema) => void;
setOpenedPlayer: ({
name,
id,
}: {
id: string | undefined;
name: string | undefined;
}) => void;
}

function Player({ player, setPlayerToDelete, setPlayerToUpdate }: PlayerProps) {
function Player({
player,
setPlayerToDelete,
setPlayerToUpdate,
setOpenedPlayer,
}: PlayerProps) {
const handleDelete = (playerId: string) => setPlayerToDelete(playerId);

return (
<Card>
<CardHeader className="flex-row items-center justify-between">
Expand All @@ -46,6 +60,19 @@ function Player({ player, setPlayerToDelete, setPlayerToUpdate }: PlayerProps) {
</DropdownMenuContent>
</DropdownMenu>
</CardHeader>
<CardContent>
<Button
variant="secondary"
onClick={() =>
setOpenedPlayer({
id: player.id,
name: player.name,
})
}
>
All time Stats
</Button>
</CardContent>
</Card>
);
}
Expand Down
18 changes: 14 additions & 4 deletions components/players/PlayerList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,21 @@ import EmptyState from "../EmptyState";
import Player from "./Player";
import AddPlayerButton from "./AddPlayer";
import AddEditPlayerFormDialog from "./AddUpdatePlayerDialog";
import PlayerStats from "./PlayerStats";

function PlayerList() {
const { players, isFetching } = useAllPlayers();
const { deletePlayer, isPending } = useDeletePlayer();
const [playerToDelete, setPlayerToDelete] = useState<string | undefined>(
undefined,
);
const [playerToDelete, setPlayerToDelete] = useState<string | undefined>();

const [playerToUpdate, setPlayerToUpdate] = useState<
UpdatePlayerSchema | undefined
>(undefined);
>();

const [openedPlayer, setOpenedPlayer] = useState<{
id: string | undefined;
name: string | undefined;
}>();

if (isFetching)
return (
Expand All @@ -48,6 +52,7 @@ function PlayerList() {
player={player}
setPlayerToDelete={setPlayerToDelete}
setPlayerToUpdate={setPlayerToUpdate}
setOpenedPlayer={setOpenedPlayer}
/>
);
})}
Expand All @@ -73,6 +78,11 @@ function PlayerList() {
content="Removing players may lead to bugs if the player is included in any matches. Do you still want to continue?"
onConfirm={() => playerToDelete && deletePlayer(playerToDelete)}
/>

<PlayerStats
openedPlayer={openedPlayer}
setOpenedPlayer={() => setOpenedPlayer(undefined)}
/>
</>
);
}
Expand Down
91 changes: 91 additions & 0 deletions components/players/PlayerStats.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { usePlayerStats } from "@/apiHooks/player";
import { Dialog, DialogContent, DialogHeader } from "../ui/dialog";

function PlayerStats({
openedPlayer,
setOpenedPlayer,
}: {
openedPlayer:
| {
id: string | undefined;
name: string | undefined;
}
| undefined;
setOpenedPlayer: (playerId: string | undefined) => void;
}) {
const playerId = openedPlayer?.id;
const playerName = openedPlayer?.name;
const { data } = usePlayerStats(playerId);

const matchesPlayed = data?.matchesPlayed;

const batStrikeRate =
Math.round(
((data?.batting.runs ?? 0) / (data?.batting.balls ?? 1)) * 1000,
) / 10;

const isNotOutYet = data?.batting.wickets === 0;
const batAverage = (data?.batting.runs ?? 0) / (matchesPlayed ?? 0) ?? 0;

return (
<Dialog
open={!!playerId}
onOpenChange={() => setOpenedPlayer(playerId ? undefined : playerId)}
>
<DialogContent>
<DialogHeader className="flex-row items-center gap-4 space-y-0">
<div className="text-lg font-bold">{playerName}</div>
<div className="text-sm font-bold">Matches - {matchesPlayed}</div>
</DialogHeader>
{data ? (
<div className="overflow-hidden rounded-md">
<div className="mb-1 bg-primary p-2 text-primary-foreground">
<h4 className="text-lg font-semibold md:text-xl">Batting</h4>
</div>
<div className="grid grid-cols-3 gap-1">
<Stat data={data.batting.runs} dataKey="Runs" />
<Stat
data={matchesPlayed ? batAverage : "-"}
dataKey="Average"
showStar={isNotOutYet}
/>
<Stat
data={data.batting.balls ? batStrikeRate : "-"}
dataKey="Strike rate"
/>
<Stat data={data.batting.fifties} dataKey="Fifties" />
<Stat data={data.batting.centuries} dataKey="Centuries" />
<Stat data={data.batting.highestScore} dataKey="High. Score" />
</div>
</div>
) : (
<p>No data found</p>
)}
</DialogContent>
</Dialog>
);
}

function Stat({
data,
dataKey,
showStar,
}: {
dataKey: string;
data: number | "-";
showStar?: boolean;
}) {
return (
<div className="bg-muted p-2">
<h5 className="font-semibold uppercase text-muted-foreground max-md:text-sm">
{dataKey}
</h5>
<p className="text-2xl font-bold max-md:text-xl">
{data}
{showStar && "*"}
</p>
</div>
);
}

export default PlayerStats;
2 changes: 1 addition & 1 deletion components/ui/dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 rounded-md border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 rounded-md border bg-background p-2 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg md:p-6",
className,
)}
{...props}
Expand Down
2 changes: 1 addition & 1 deletion services/player/getPlayerById.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { axiosInstance } from "../axiosInstance";

export const getPlayerById = async (id: Player["id"] | null | undefined) => {
try {
if (!id || id === "dummy") throw new Error("Player not found");
if (!id) throw new Error("Player not found");

const res = await axiosInstance.get(`/players/${id}`);
if (res.status !== 200) {
Expand Down
16 changes: 16 additions & 0 deletions services/player/getPlayerStats.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { axiosInstance } from "../axiosInstance";
import { PlayerStats } from "@/types";

export const getPlayerStats = async (id: string | null | undefined) => {
try {
if (!id) throw new Error("Player not found");

const res = await axiosInstance.get(`/players/stats/${id}`);
if (res.status !== 200) throw new Error("Network response was not ok");

return res.data as PlayerStats;
} catch (error) {
console.error("Error while fetching a player:", error);
throw new Error((error as Error).message);
}
};
Loading

0 comments on commit aca5586

Please sign in to comment.