Skip to content

Commit

Permalink
Merge pull request #324 from TaloDev/develop
Browse files Browse the repository at this point in the history
Release 0.42.0
  • Loading branch information
tudddorrr authored Jan 20, 2025
2 parents 439c5e4 + 8c7fe1f commit 7767791
Show file tree
Hide file tree
Showing 9 changed files with 182 additions and 21 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@ Talo is available to use via our [Godot plugin](https://github.com/TaloDev/godot
- 🕹️ [Leaderboards](https://trytalo.com/leaderboards): Highly customisable leaderboards that can sync with Steamworks.
- 💾 [Game saves](https://trytalo.com/saves): A simple and flexible way to load/save game state; also works offline.
- 📊 [Game stats](https://trytalo.com/stats): Track global or per-player stats across your game; also syncs with Steamworks.
- 💬 [Game channels](https://trytalo.com/channels): Send real-time messages between players subscribed to specific topics.
- ⚙️ [Live config](https://trytalo.com/live-config): Update game settings from the web with zero downtime.
- 🔧 [Steamworks integration](https://trytalo.com/steamworks-integration): Hook into Steamworks for authentication and ownership checks.
- 💬 [Game feedback](https://trytalo.com/feedback): Collect and manage feedback from your players.
- 🗣️ [Game feedback](https://trytalo.com/feedback): Collect and manage feedback from your players.

## Documentation

Expand Down
39 changes: 26 additions & 13 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@
"lint-staged": {
"*.{ts,js,tsx,jsx}": "eslint --fix"
},
"version": "0.41.0",
"version": "0.42.0",
"engines": {
"node": "20.x"
},
Expand Down
15 changes: 15 additions & 0 deletions src/api/updateStatValue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { z } from 'zod'
import api from './api'
import makeValidatedRequest from './makeValidatedRequest'
import { playerGameStatSchema } from '../entities/playerGameStat'

const updateStatValue = makeValidatedRequest(
(gameId: number, playerId: string, playerStatId: number, newValue: number) => {
return api.patch(`/games/${gameId}/players/${playerId}/stats/${playerStatId}`, { newValue })
},
z.object({
playerStat: playerGameStatSchema
})
)

export default updateStatValue
5 changes: 3 additions & 2 deletions src/api/usePlayerStats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const usePlayerStats = (activeGame: Game, playerId: string) => {
return res
}

const { data, error } = useSWR(
const { data, error, mutate } = useSWR(
[`/games/${activeGame.id}/players/${playerId}/stats`],
fetcher
)
Expand All @@ -23,7 +23,8 @@ const usePlayerStats = (activeGame: Game, playerId: string) => {
stats: data?.stats ?? [],
loading: !data && !error,
error: error && buildError(error),
errorStatusCode: error && error.response?.status
errorStatusCode: error && error.response?.status,
mutate
}
}

Expand Down
1 change: 1 addition & 0 deletions src/components/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export default function Footer() {
<li><Link to='https://trytalo.com/players?utm_source=dashboard&utm_medium=footer'>Player management</Link></li>
<li><Link to='https://trytalo.com/events?utm_source=dashboard&utm_medium=footer'>Event tracking</Link></li>
<li><Link to='https://trytalo.com/leaderboards?utm_source=dashboard&utm_medium=footer'>Leaderboards</Link></li>
<li><Link to='https://trytalo.com/channels?utm_source=dashboard&utm_medium=footer'>Game channels</Link></li>
<li><Link to='https://trytalo.com/saves?utm_source=dashboard&utm_medium=footer'>Game saves</Link></li>
<li><Link to='https://trytalo.com/stats?utm_source=dashboard&utm_medium=footer'>Game stats</Link></li>
<li><Link to='https://trytalo.com/feedback?utm_source=dashboard&utm_medium=footer'>Game feedback</Link></li>
Expand Down
95 changes: 95 additions & 0 deletions src/modals/UpdateStatValue.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { MouseEvent, useState } from 'react'
import Modal from '../components/Modal'
import TextInput from '../components/TextInput'
import Button from '../components/Button'
import buildError from '../utils/buildError'
import ErrorMessage, { TaloError } from '../components/ErrorMessage'
import { PlayerGameStat } from '../entities/playerGameStat'
import { KeyedMutator } from 'swr/_internal'
import updateStatValue from '../api/updateStatValue'
import { Player } from '../entities/player'
import activeGameState from '../state/activeGameState'
import { useRecoilValue } from 'recoil'
import { SelectedActiveGame } from '../state/activeGameState'
import { upperFirst } from 'lodash-es'

type UpdateStatValueProps = {
modalState: [boolean, (open: boolean) => void]
mutate: KeyedMutator<{ stats: PlayerGameStat[] }>
editingStat: PlayerGameStat
player: Player
}

export default function UpdateStatValue({ modalState, mutate, editingStat, player }: UpdateStatValueProps) {
const [, setOpen] = modalState
const [value, setValue] = useState(editingStat.value.toString())
const [isLoading, setLoading] = useState(false)
const [error, setError] = useState<TaloError | null>(null)

const activeGame = useRecoilValue(activeGameState) as SelectedActiveGame

const onCreateClick = async (e: MouseEvent<HTMLElement>) => {
e.preventDefault()
setLoading(true)
setError(null)

try {
const { playerStat } = await updateStatValue(activeGame.id, player.id, editingStat.id, Number(value))
mutate((data) => {
return {
...data,
stats: [...data!.stats.filter((stat) => stat.id !== editingStat!.id), playerStat]
}
}, true)
setOpen(false)
} catch (err) {
setError(buildError(err))
setLoading(false)
}
}

return (
<Modal
id='update-player-stat'
title={upperFirst(editingStat.stat.name)}
modalState={modalState}
>
<form>
<div className='p-4 space-y-4'>
<div>
<p className='font-semibold'>Current value</p>
<p>{editingStat.value}</p>
</div>

<TextInput
id='value'
variant='light'
type='number'
label='New value'
placeholder='Stat value'
onChange={setValue}
value={value}
inputClassName='border border-gray-200 focus:border-opacity-0'
/>

{error && <ErrorMessage error={error} />}
</div>

<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'>
<div className='w-full md:w-32'>
<Button
disabled={!value}
isLoading={isLoading}
onClick={onCreateClick}
>
Update
</Button>
</div>
<div className='w-full md:w-32'>
<Button type='button' variant='grey' onClick={() => setOpen(false)}>Cancel</Button>
</div>
</div>
</form>
</Modal>
)
}
39 changes: 36 additions & 3 deletions src/pages/PlayerStats.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect } from 'react'
import { useEffect, useState } from 'react'
import { useNavigate, useParams } from 'react-router-dom'
import ErrorMessage from '../components/ErrorMessage'
import TableCell from '../components/tables/TableCell'
Expand All @@ -14,20 +14,31 @@ import usePlayer from '../utils/usePlayer'
import Table from '../components/tables/Table'
import { useRecoilValue } from 'recoil'
import activeGameState, { SelectedActiveGame } from '../state/activeGameState'
import Button from '../components/Button'
import { IconPencil } from '@tabler/icons-react'
import { PlayerGameStat } from '../entities/playerGameStat'
import UpdateStatValue from '../modals/UpdateStatValue'
import { PermissionBasedAction } from '../utils/canPerformAction'
import canPerformAction from '../utils/canPerformAction'
import userState from '../state/userState'
import { AuthedUser } from '../state/userState'

export default function PlayerStats() {
const user = useRecoilValue(userState) as AuthedUser
const activeGame = useRecoilValue(activeGameState) as SelectedActiveGame

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

const { stats, loading: statsLoading, error, errorStatusCode } = usePlayerStats(activeGame, playerId!)
const { stats, loading: statsLoading, error, errorStatusCode, mutate } = usePlayerStats(activeGame, playerId!)
const sortedStats = useSortedItems(stats, 'updatedAt')

const navigate = useNavigate()

const loading = !player || statsLoading

const [editingStat, setEditingStat] = useState<PlayerGameStat | null>(null)

useEffect(() => {
if (errorStatusCode === 404) {
navigate(routes.players, { replace: true })
Expand All @@ -52,7 +63,20 @@ export default function PlayerStats() {
{(playerStat) => (
<>
<TableCell className='min-w-60'>{playerStat.stat.name}</TableCell>
<TableCell className='min-w-40'>{playerStat.value}</TableCell>
<TableCell className='min-w-40 flex items-center space-x-2'>
<>
<span>{playerStat.value}</span>
{canPerformAction(user, PermissionBasedAction.UPDATE_PLAYER_STAT) &&
<Button
variant='icon'
className='p-1 rounded-full bg-indigo-900'
onClick={() => setEditingStat(playerStat)}
icon={<IconPencil size={16} />}
extra={{ 'aria-label': 'Edit game name' }}
/>
}
</>
</TableCell>
<DateCell>{format(new Date(playerStat.createdAt), 'dd MMM Y, HH:mm')}</DateCell>
<DateCell>{format(new Date(playerStat.updatedAt), 'dd MMM Y, HH:mm')}</DateCell>
</>
Expand All @@ -61,6 +85,15 @@ export default function PlayerStats() {
</Table>
}

{editingStat &&
<UpdateStatValue
modalState={[true, () => setEditingStat(null)]}
mutate={mutate}
editingStat={editingStat}
player={player}
/>
}

{error && <ErrorMessage error={error} />}
</Page>
)
Expand Down
4 changes: 3 additions & 1 deletion src/utils/canPerformAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ export enum PermissionBasedAction {
DELETE_STAT,
DELETE_GROUP,
DELETE_FEEDBACK_CATEGORY,
VIEW_PLAYER_AUTH_ACTIVITIES
VIEW_PLAYER_AUTH_ACTIVITIES,
UPDATE_PLAYER_STAT
}

export default function canPerformAction(user: User, action: PermissionBasedAction) {
Expand All @@ -17,6 +18,7 @@ export default function canPerformAction(user: User, action: PermissionBasedActi
case PermissionBasedAction.DELETE_STAT:
case PermissionBasedAction.DELETE_FEEDBACK_CATEGORY:
case PermissionBasedAction.VIEW_PLAYER_AUTH_ACTIVITIES:
case PermissionBasedAction.UPDATE_PLAYER_STAT:
return user.type === UserType.ADMIN
case PermissionBasedAction.DELETE_GROUP:
return [UserType.DEV, UserType.ADMIN].includes(user.type)
Expand Down

0 comments on commit 7767791

Please sign in to comment.