Skip to content

Commit

Permalink
Merge pull request #138 from daithihearn/player-stats
Browse files Browse the repository at this point in the history
Player stats
  • Loading branch information
daithihearn authored Feb 27, 2023
2 parents 0f0772a + e65148d commit acd21d3
Show file tree
Hide file tree
Showing 11 changed files with 145 additions and 130 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "frontend",
"version": "5.3.4",
"version": "5.4.0",
"description": "React frontend for the Cards 110",
"author": "Daithi Hearn",
"license": "MIT",
Expand Down
2 changes: 1 addition & 1 deletion public/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"short_name": "Cards 110",
"name": "Cards 110",
"version": "5.3.4",
"version": "5.4.0",
"icons": [
{
"src": "./assets/favicon.png",
Expand Down
34 changes: 0 additions & 34 deletions src/caches/GameStatsSlice.ts

This file was deleted.

2 changes: 0 additions & 2 deletions src/caches/caches.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {

import { myProfileSlice } from "./MyProfileSlice"
import { gameSlice } from "./GameSlice"
import { gameStatsSlice } from "./GameStatsSlice"
import { myGamesSlice } from "./MyGamesSlice"
import { myCardsSlice } from "./MyCardsSlice"
import { autoPlaySlice } from "./AutoPlaySlice"
Expand All @@ -19,7 +18,6 @@ const combinedReducer = combineReducers({
myProfile: myProfileSlice.reducer,
game: gameSlice.reducer,
myGames: myGamesSlice.reducer,
gameStats: gameStatsSlice.reducer,
playerProfiles: playerProfilesSlice.reducer,
myCards: myCardsSlice.reducer,
autoPlay: autoPlaySlice.reducer,
Expand Down
80 changes: 13 additions & 67 deletions src/components/GameStats/GameStats.tsx
Original file line number Diff line number Diff line change
@@ -1,94 +1,40 @@
import { Doughnut } from "react-chartjs-2"
import { Card, CardHeader, CardBody, CardGroup, Input, Label } from "reactstrap"
import { useCallback, useMemo, useState } from "react"
import { useCallback, useEffect, useState } from "react"
import { useAppSelector } from "../../caches/hooks"
import { getGameStats } from "../../caches/GameStatsSlice"
import "chart.js/auto"
import { ChartOptions } from "chart.js"
import { getMyProfile } from "../../caches/MyProfileSlice"
import PlayerSwitcher from "./PlayerSwitcher"
import WinPercentageGraph from "./WinPercentageGraph"
import { PlayerProfile } from "../../model/Player"

const GameStats = () => {
const myProfile = useAppSelector(getMyProfile)
const stats = useAppSelector(getGameStats)

const [player, setPlayer] = useState<PlayerProfile>()
const [last3Months, updateLast3Months] = useState(true)

const fromDate = new Date()
fromDate.setMonth(fromDate.getMonth() - 3)

const filteredStats = useMemo(
() =>
last3Months
? stats.filter(s => new Date(s.timestamp) >= fromDate)
: stats,
[last3Months, stats],
)

const wins = useMemo(
() => filteredStats.filter(g => g.winner),
[filteredStats],
)

const data = useMemo(() => {
return {
labels: ["Win", "Loss"],
datasets: [
{
label: "My Win Percentage",
data: [wins.length, filteredStats.length - wins.length],
backgroundColor: ["rgb(54, 162, 235)", "rgb(255, 99, 132)"],
hoverOffset: 4,
},
],
}
}, [wins, filteredStats])

const options: ChartOptions = useMemo(() => {
return {
maintainAspectRatio: false,
plugins: {
title: {
display: true,
text: `Win Percentage (${(
(wins.length / filteredStats.length) *
100
).toFixed(1)}%)`,
position: "bottom",
},
},
}
}, [wins, filteredStats])

useEffect(() => setPlayer(myProfile), [myProfile])
const threeMonthsCheckboxChanged = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
updateLast3Months(e.target.checked)
},
[],
)

if (!stats) {
return null
}

return (
<CardGroup>
<Card className="p-6 data-card">
<CardHeader tag="h2">Stats </CardHeader>
<CardBody>
{myProfile.isAdmin ? <PlayerSwitcher /> : null}
{myProfile.isAdmin ? (
<PlayerSwitcher onChange={setPlayer} />
) : null}
</CardBody>
<CardBody>
{filteredStats.length > 0 ? (
<Doughnut
data={data}
options={options}
width={300}
height={300}
{player ? (
<WinPercentageGraph
player={player}
last3Months={last3Months}
/>
) : (
"No stats available currently"
)}
) : null}
</CardBody>
<Label>
<Input
Expand Down
21 changes: 7 additions & 14 deletions src/components/GameStats/PlayerSwitcher.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
import { useSnackbar } from "notistack"
import { useCallback, useEffect, useMemo, useState } from "react"
import {
Dropdown,
DropdownItem,
DropdownMenu,
DropdownToggle,
} from "reactstrap"
import { useAppDispatch, useAppSelector } from "../../caches/hooks"
import { useAppSelector } from "../../caches/hooks"
import { getMyProfile } from "../../caches/MyProfileSlice"
import { getPlayerProfiles } from "../../caches/PlayerProfilesSlice"
import { PlayerProfile } from "../../model/Player"
import StatsService from "../../services/StatsService"
import { FormatName } from "../../utils/FormattingUtils"

const PlayerSwitcher: React.FC = () => {
const dispatch = useAppDispatch()
interface Props {
onChange: (player: PlayerProfile) => void
}

const PlayerSwitcher: React.FC<Props> = ({ onChange }) => {
const myProfile = useAppSelector(getMyProfile)
const players = useAppSelector(getPlayerProfiles)
const [showDropdown, setShowDropdown] = useState(false)
const [currentPlayer, setCurrentPlayer] = useState<PlayerProfile>()
const { enqueueSnackbar } = useSnackbar()

const toggleDropdown = useCallback(
() => setShowDropdown(!showDropdown),
Expand All @@ -43,14 +43,7 @@ const PlayerSwitcher: React.FC = () => {
}, [sortedPlayers, myProfile])

useEffect(() => {
if (currentPlayer)
dispatch(StatsService.gameStatsForPlayer(currentPlayer.id)).catch(
e =>
enqueueSnackbar(
`Failed to get game stats for player ${currentPlayer.name}`,
{ variant: "error" },
),
)
if (currentPlayer) onChange(currentPlayer)
}, [currentPlayer])

return (
Expand Down
92 changes: 92 additions & 0 deletions src/components/GameStats/WinPercentageGraph.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import React, { useEffect, useMemo, useState } from "react"
import { PlayerGameStats, PlayerProfile } from "../../model/Player"
import { Doughnut } from "react-chartjs-2"
import "chart.js/auto"
import { ChartOptions } from "chart.js"
import { useAppDispatch } from "../../caches/hooks"
import { useSnackbar } from "notistack"
import StatsService from "../../services/StatsService"

interface Props {
player: PlayerProfile
last3Months: boolean
}

const WinPercentageGraph: React.FC<Props> = ({ player, last3Months }) => {
const dispatch = useAppDispatch()
const { enqueueSnackbar } = useSnackbar()
const [stats, setStats] = useState<PlayerGameStats[]>([])

useEffect(() => {
dispatch(StatsService.gameStatsForPlayer(player.id))
.then(setStats)
.catch(e =>
enqueueSnackbar(
`Failed to get game stats for player ${player.name}`,
{ variant: "error" },
),
)
}, [player])

const filteredStats = useMemo(() => {
const fromDate = new Date()
fromDate.setMonth(fromDate.getMonth() - 3)
return last3Months
? stats.filter(s => new Date(s.timestamp) >= fromDate)
: stats
}, [stats, last3Months])

const wins = useMemo(
() => filteredStats.filter(g => g.winner),
[filteredStats],
)

const data = useMemo(() => {
return {
labels: ["Win", "Loss"],
datasets: [
{
label: "My Win Percentage",
data: [wins.length, filteredStats.length - wins.length],
backgroundColor: ["rgb(54, 162, 235)", "rgb(255, 99, 132)"],
hoverOffset: 4,
},
],
}
}, [wins, filteredStats])

const options: ChartOptions = useMemo(() => {
return {
maintainAspectRatio: false,
plugins: {
title: {
display: true,
text: `Win Percentage (${(
(wins.length / filteredStats.length) *
100
).toFixed(1)}%)`,
position: "bottom",
},
},
}
}, [wins, filteredStats])

return (
<>
{filteredStats.length > 0 ? (
<>
<Doughnut
data={data}
options={options}
width={300}
height={300}
/>
</>
) : (
"No stats available currently"
)}
</>
)
}

export default WinPercentageGraph
30 changes: 23 additions & 7 deletions src/components/StartNewGame/StartNewGame.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import DataTable, { TableColumn } from "react-data-table-component"
import { getPlayerProfiles } from "../../caches/PlayerProfilesSlice"

import {
Label,
Button,
ButtonGroup,
Form,
Expand All @@ -15,6 +14,7 @@ import {
CardBody,
CardGroup,
CardHeader,
Label,
} from "reactstrap"

import { useAppDispatch, useAppSelector } from "../../caches/hooks"
Expand All @@ -24,6 +24,8 @@ import { customStyles } from "../Tables/CustomStyles"
import parseError from "../../utils/ErrorUtils"
import moment from "moment"
import { FormatName } from "../../utils/FormattingUtils"
import WinPercentageGraph from "../GameStats/WinPercentageGraph"
import { Divider } from "@mui/material"

const StartNewGame = () => {
const dispatch = useAppDispatch()
Expand Down Expand Up @@ -86,18 +88,32 @@ const StartNewGame = () => {
)

const columns: TableColumn<PlayerProfile>[] = [
{
name: "Avatar",
cell: (row: PlayerProfile) => (
<img alt="Image Preview" src={row.picture} className="avatar" />
),
},
{
name: "Player",
selector: row => row.name,
cell: (row: PlayerProfile) => (
<div>
<img
alt="Image Preview"
src={row.picture}
className="avatar"
/>
<Divider />
<span>
<b>{FormatName(row.name)}</b>
</span>
</div>
),
format: row => FormatName(row.name),
sortable: true,
},
{
name: "Stats (3 months)",
cell: (pp: PlayerProfile) => (
<WinPercentageGraph player={pp} last3Months={true} />
),
center: true,
},
{
name: "Last Access",
id: "lastAccess",
Expand Down
1 change: 1 addition & 0 deletions src/components/Tables/CustomStyles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const customStyles = {
borderRadius: "25px",
outline: "1px solid #FFFFFF",
},
maxHeight: "2em",
},
pagination: {
style: {
Expand Down
4 changes: 4 additions & 0 deletions src/pages/Home/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ const Home = () => {
<Divider />
<Divider />
<Divider />
</>
) : null}
{myProfile.isPlayer && !myProfile.isAdmin ? (
<>
<GameStats />
<Divider />
<Divider />
Expand Down
Loading

0 comments on commit acd21d3

Please sign in to comment.