Skip to content

Commit 91476ff

Browse files
committed
update player game stat value modal
1 parent d80bf6a commit 91476ff

File tree

5 files changed

+151
-6
lines changed

5 files changed

+151
-6
lines changed

src/api/updateStatValue.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { z } from 'zod'
2+
import api from './api'
3+
import makeValidatedRequest from './makeValidatedRequest'
4+
import { playerGameStatSchema } from '../entities/playerGameStat'
5+
6+
const updateStatValue = makeValidatedRequest(
7+
(gameId: number, playerId: string, playerStatId: number, newValue: number) => {
8+
return api.patch(`/games/${gameId}/players/${playerId}/stats/${playerStatId}`, { newValue })
9+
},
10+
z.object({
11+
playerStat: playerGameStatSchema
12+
})
13+
)
14+
15+
export default updateStatValue

src/api/usePlayerStats.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const usePlayerStats = (activeGame: Game, playerId: string) => {
1414
return res
1515
}
1616

17-
const { data, error } = useSWR(
17+
const { data, error, mutate } = useSWR(
1818
[`/games/${activeGame.id}/players/${playerId}/stats`],
1919
fetcher
2020
)
@@ -23,7 +23,8 @@ const usePlayerStats = (activeGame: Game, playerId: string) => {
2323
stats: data?.stats ?? [],
2424
loading: !data && !error,
2525
error: error && buildError(error),
26-
errorStatusCode: error && error.response?.status
26+
errorStatusCode: error && error.response?.status,
27+
mutate
2728
}
2829
}
2930

src/modals/UpdateStatValue.tsx

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { MouseEvent, useState } from 'react'
2+
import Modal from '../components/Modal'
3+
import TextInput from '../components/TextInput'
4+
import Button from '../components/Button'
5+
import buildError from '../utils/buildError'
6+
import ErrorMessage, { TaloError } from '../components/ErrorMessage'
7+
import { PlayerGameStat } from '../entities/playerGameStat'
8+
import { KeyedMutator } from 'swr/_internal'
9+
import updateStatValue from '../api/updateStatValue'
10+
import { Player } from '../entities/player'
11+
import activeGameState from '../state/activeGameState'
12+
import { useRecoilValue } from 'recoil'
13+
import { SelectedActiveGame } from '../state/activeGameState'
14+
15+
type UpdateStatValueProps = {
16+
modalState: [boolean, (open: boolean) => void]
17+
mutate: KeyedMutator<{ stats: PlayerGameStat[] }>
18+
editingStat: PlayerGameStat
19+
player: Player
20+
}
21+
22+
export default function UpdateStatValue({ modalState, mutate, editingStat, player }: UpdateStatValueProps) {
23+
const [, setOpen] = modalState
24+
const [value, setValue] = useState(editingStat.value.toString())
25+
const [isLoading, setLoading] = useState(false)
26+
const [error, setError] = useState<TaloError | null>(null)
27+
28+
const activeGame = useRecoilValue(activeGameState) as SelectedActiveGame
29+
30+
const onCreateClick = async (e: MouseEvent<HTMLElement>) => {
31+
e.preventDefault()
32+
setLoading(true)
33+
setError(null)
34+
35+
try {
36+
const { playerStat } = await updateStatValue(activeGame.id, player.id, editingStat.id, Number(value))
37+
mutate((data) => {
38+
return {
39+
...data,
40+
stats: [...data!.stats.filter((stat) => stat.id !== editingStat!.id), playerStat]
41+
}
42+
}, true)
43+
setOpen(false)
44+
} catch (err) {
45+
setError(buildError(err))
46+
setLoading(false)
47+
}
48+
}
49+
50+
return (
51+
<Modal
52+
id='update-stat-value'
53+
title={`Update ${editingStat.stat.name} value`}
54+
modalState={modalState}
55+
>
56+
<form>
57+
<div className='p-4 space-y-4'>
58+
<div>
59+
<p className='font-semibold'>Current value</p>
60+
<p>{editingStat.value}</p>
61+
</div>
62+
63+
<TextInput
64+
id='value'
65+
variant='light'
66+
type='number'
67+
label='New value'
68+
placeholder='Stat value'
69+
onChange={setValue}
70+
value={value}
71+
inputClassName='border border-gray-200 focus:border-opacity-0'
72+
/>
73+
74+
{error && <ErrorMessage error={error} />}
75+
</div>
76+
77+
<div className='flex flex-col md:flex-row-reverse md:justify-between space-y-4 md:space-y-0 p-4 border-t border-gray-200'>
78+
<div className='w-full md:w-32'>
79+
<Button
80+
disabled={!value}
81+
isLoading={isLoading}
82+
onClick={onCreateClick}
83+
>
84+
Update
85+
</Button>
86+
</div>
87+
<div className='w-full md:w-32'>
88+
<Button type='button' variant='grey' onClick={() => setOpen(false)}>Cancel</Button>
89+
</div>
90+
</div>
91+
</form>
92+
</Modal>
93+
)
94+
}

src/pages/PlayerStats.tsx

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useEffect } from 'react'
1+
import { useEffect, useState } from 'react'
22
import { useNavigate, useParams } from 'react-router-dom'
33
import ErrorMessage from '../components/ErrorMessage'
44
import TableCell from '../components/tables/TableCell'
@@ -14,20 +14,31 @@ import usePlayer from '../utils/usePlayer'
1414
import Table from '../components/tables/Table'
1515
import { useRecoilValue } from 'recoil'
1616
import activeGameState, { SelectedActiveGame } from '../state/activeGameState'
17+
import Button from '../components/Button'
18+
import { IconPencil } from '@tabler/icons-react'
19+
import { PlayerGameStat } from '../entities/playerGameStat'
20+
import UpdateStatValue from '../modals/UpdateStatValue'
21+
import { PermissionBasedAction } from '../utils/canPerformAction'
22+
import canPerformAction from '../utils/canPerformAction'
23+
import userState from '../state/userState'
24+
import { AuthedUser } from '../state/userState'
1725

1826
export default function PlayerStats() {
27+
const user = useRecoilValue(userState) as AuthedUser
1928
const activeGame = useRecoilValue(activeGameState) as SelectedActiveGame
2029

2130
const { id: playerId } = useParams()
2231
const [player] = usePlayer()
2332

24-
const { stats, loading: statsLoading, error, errorStatusCode } = usePlayerStats(activeGame, playerId!)
33+
const { stats, loading: statsLoading, error, errorStatusCode, mutate } = usePlayerStats(activeGame, playerId!)
2534
const sortedStats = useSortedItems(stats, 'updatedAt')
2635

2736
const navigate = useNavigate()
2837

2938
const loading = !player || statsLoading
3039

40+
const [editingStat, setEditingStat] = useState<PlayerGameStat | null>(null)
41+
3142
useEffect(() => {
3243
if (errorStatusCode === 404) {
3344
navigate(routes.players, { replace: true })
@@ -52,7 +63,20 @@ export default function PlayerStats() {
5263
{(playerStat) => (
5364
<>
5465
<TableCell className='min-w-60'>{playerStat.stat.name}</TableCell>
55-
<TableCell className='min-w-40'>{playerStat.value}</TableCell>
66+
<TableCell className='min-w-40 flex items-center space-x-2'>
67+
<>
68+
<span>{playerStat.value}</span>
69+
{canPerformAction(user, PermissionBasedAction.UPDATE_PLAYER_STAT) &&
70+
<Button
71+
variant='icon'
72+
className='p-1 rounded-full bg-indigo-900'
73+
onClick={() => setEditingStat(playerStat)}
74+
icon={<IconPencil size={16} />}
75+
extra={{ 'aria-label': 'Edit game name' }}
76+
/>
77+
}
78+
</>
79+
</TableCell>
5680
<DateCell>{format(new Date(playerStat.createdAt), 'dd MMM Y, HH:mm')}</DateCell>
5781
<DateCell>{format(new Date(playerStat.updatedAt), 'dd MMM Y, HH:mm')}</DateCell>
5882
</>
@@ -61,6 +85,15 @@ export default function PlayerStats() {
6185
</Table>
6286
}
6387

88+
{editingStat &&
89+
<UpdateStatValue
90+
modalState={[true, () => setEditingStat(null)]}
91+
mutate={mutate}
92+
editingStat={editingStat}
93+
player={player}
94+
/>
95+
}
96+
6497
{error && <ErrorMessage error={error} />}
6598
</Page>
6699
)

src/utils/canPerformAction.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ export enum PermissionBasedAction {
66
DELETE_STAT,
77
DELETE_GROUP,
88
DELETE_FEEDBACK_CATEGORY,
9-
VIEW_PLAYER_AUTH_ACTIVITIES
9+
VIEW_PLAYER_AUTH_ACTIVITIES,
10+
UPDATE_PLAYER_STAT
1011
}
1112

1213
export default function canPerformAction(user: User, action: PermissionBasedAction) {
@@ -17,6 +18,7 @@ export default function canPerformAction(user: User, action: PermissionBasedActi
1718
case PermissionBasedAction.DELETE_STAT:
1819
case PermissionBasedAction.DELETE_FEEDBACK_CATEGORY:
1920
case PermissionBasedAction.VIEW_PLAYER_AUTH_ACTIVITIES:
21+
case PermissionBasedAction.UPDATE_PLAYER_STAT:
2022
return user.type === UserType.ADMIN
2123
case PermissionBasedAction.DELETE_GROUP:
2224
return [UserType.DEV, UserType.ADMIN].includes(user.type)

0 commit comments

Comments
 (0)