Skip to content

Commit 7767791

Browse files
authored
Merge pull request #324 from TaloDev/develop
Release 0.42.0
2 parents 439c5e4 + 8c7fe1f commit 7767791

File tree

9 files changed

+182
-21
lines changed

9 files changed

+182
-21
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,10 @@ Talo is available to use via our [Godot plugin](https://github.com/TaloDev/godot
1616
- 🕹️ [Leaderboards](https://trytalo.com/leaderboards): Highly customisable leaderboards that can sync with Steamworks.
1717
- 💾 [Game saves](https://trytalo.com/saves): A simple and flexible way to load/save game state; also works offline.
1818
- 📊 [Game stats](https://trytalo.com/stats): Track global or per-player stats across your game; also syncs with Steamworks.
19+
- 💬 [Game channels](https://trytalo.com/channels): Send real-time messages between players subscribed to specific topics.
1920
- ⚙️ [Live config](https://trytalo.com/live-config): Update game settings from the web with zero downtime.
2021
- 🔧 [Steamworks integration](https://trytalo.com/steamworks-integration): Hook into Steamworks for authentication and ownership checks.
21-
- 💬 [Game feedback](https://trytalo.com/feedback): Collect and manage feedback from your players.
22+
- 🗣️ [Game feedback](https://trytalo.com/feedback): Collect and manage feedback from your players.
2223

2324
## Documentation
2425

package-lock.json

Lines changed: 26 additions & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@
7878
"lint-staged": {
7979
"*.{ts,js,tsx,jsx}": "eslint --fix"
8080
},
81-
"version": "0.41.0",
81+
"version": "0.42.0",
8282
"engines": {
8383
"node": "20.x"
8484
},

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/components/Footer.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export default function Footer() {
2727
<li><Link to='https://trytalo.com/players?utm_source=dashboard&utm_medium=footer'>Player management</Link></li>
2828
<li><Link to='https://trytalo.com/events?utm_source=dashboard&utm_medium=footer'>Event tracking</Link></li>
2929
<li><Link to='https://trytalo.com/leaderboards?utm_source=dashboard&utm_medium=footer'>Leaderboards</Link></li>
30+
<li><Link to='https://trytalo.com/channels?utm_source=dashboard&utm_medium=footer'>Game channels</Link></li>
3031
<li><Link to='https://trytalo.com/saves?utm_source=dashboard&utm_medium=footer'>Game saves</Link></li>
3132
<li><Link to='https://trytalo.com/stats?utm_source=dashboard&utm_medium=footer'>Game stats</Link></li>
3233
<li><Link to='https://trytalo.com/feedback?utm_source=dashboard&utm_medium=footer'>Game feedback</Link></li>

src/modals/UpdateStatValue.tsx

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

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)