Skip to content

Commit

Permalink
Merge pull request #328 from TaloDev/develop
Browse files Browse the repository at this point in the history
Release 0.43.0
  • Loading branch information
tudddorrr authored Jan 21, 2025
2 parents 7767791 + e087a49 commit a0ad8ce
Show file tree
Hide file tree
Showing 11 changed files with 164 additions and 29 deletions.
4 changes: 2 additions & 2 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.42.0",
"version": "0.43.0",
"engines": {
"node": "20.x"
},
Expand Down
2 changes: 1 addition & 1 deletion src/api/updateLeaderboardEntry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import api from './api'
import makeValidatedRequest from './makeValidatedRequest'

const updateLeaderboardEntry = makeValidatedRequest(
(gameId: number, leaderboardId: number, entryId: number, data: { hidden: boolean }) =>
(gameId: number, leaderboardId: number, entryId: number, data: { hidden?: boolean, newScore?: number }) =>
api.patch(`/games/${gameId}/leaderboards/${leaderboardId}/entries/${entryId}`, data),
z.object({
entry: leaderboardEntrySchema
Expand Down
4 changes: 2 additions & 2 deletions src/api/updateStatValue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ 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 })
(gameId: number, statId: number, playerStatId: number, newValue: number) => {
return api.patch(`/games/${gameId}/game-stats/${statId}/player-stats/${playerStatId}`, { newValue })
},
z.object({
playerStat: playerGameStatSchema
Expand Down
11 changes: 7 additions & 4 deletions src/api/usePlayerLeaderboardEntries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,20 @@ export default function usePlayerLeaderboardEntries(activeGame: Game, leaderboar
entries: z.array(leaderboardEntrySchema)
}))))

return res.flatMap((res) => res.entries)
return {
entries: res.flatMap((res) => res.entries)
}
}

const { data, error } = useSWR(
const { data, error, mutate } = useSWR(
leaderboards && player ? [activeGame, leaderboards, player.aliases] : null,
fetcher
)

return {
entries: data ?? [],
entries: data?.entries ?? [],
loading: !data && !error,
error: error && buildError(error)
error: error && buildError(error),
mutate
}
}
94 changes: 94 additions & 0 deletions src/modals/UpdateEntryScore.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
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 { LeaderboardEntry } from '../entities/leaderboardEntry'
import { useRecoilValue } from 'recoil'
import activeGameState, { SelectedActiveGame } from '../state/activeGameState'
import updateLeaderboardEntry from '../api/updateLeaderboardEntry'
import { Leaderboard } from '../entities/leaderboard'
import { upperFirst } from 'lodash-es'
import { KeyedMutator } from 'swr'

type UpdateEntryScoreProps = {
modalState: [boolean, (open: boolean) => void]
mutate: KeyedMutator<{ entries: LeaderboardEntry[] }>
editingEntry: LeaderboardEntry
leaderboard: Leaderboard
}

export default function UpdateEntryScore({ modalState, mutate, editingEntry, leaderboard }: UpdateEntryScoreProps) {
const [, setOpen] = modalState
const [score, setScore] = useState(editingEntry.score.toString())
const [isLoading, setLoading] = useState(false)
const [error, setError] = useState<TaloError | null>(null)

const activeGame = useRecoilValue(activeGameState) as SelectedActiveGame

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

try {
const { entry } = await updateLeaderboardEntry(activeGame.id, leaderboard.id, editingEntry.id, { newScore: Number(score) })
mutate((data) => {
return {
...data,
entries: [...data!.entries.filter((e) => e.id !== editingEntry.id), entry]
}
}, true)
setOpen(false)
} catch (err) {
setError(buildError(err))
setLoading(false)
}
}

return (
<Modal
id='update-entry-score'
title={upperFirst(editingEntry.leaderboardName)}
modalState={modalState}
>
<form>
<div className='p-4 space-y-4'>
<div>
<p className='font-semibold'>Current score</p>
<p>{editingEntry.score}</p>
</div>

<TextInput
id='score'
variant='light'
type='number'
label='New score'
placeholder='Enter new score'
onChange={setScore}
value={score}
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={!score}
isLoading={isLoading}
onClick={onUpdateClick}
>
Update
</Button>
</div>
<div className='w-full md:w-32'>
<Button type='button' variant='grey' onClick={() => setOpen(false)}>Cancel</Button>
</div>
</div>
</form>
</Modal>
)
}
8 changes: 3 additions & 5 deletions src/modals/UpdateStatValue.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@ 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 { KeyedMutator } from 'swr'
import updateStatValue from '../api/updateStatValue'
import { Player } from '../entities/player'
import activeGameState from '../state/activeGameState'
import { useRecoilValue } from 'recoil'
import { SelectedActiveGame } from '../state/activeGameState'
Expand All @@ -17,10 +16,9 @@ type UpdateStatValueProps = {
modalState: [boolean, (open: boolean) => void]
mutate: KeyedMutator<{ stats: PlayerGameStat[] }>
editingStat: PlayerGameStat
player: Player
}

export default function UpdateStatValue({ modalState, mutate, editingStat, player }: UpdateStatValueProps) {
export default function UpdateStatValue({ modalState, mutate, editingStat }: UpdateStatValueProps) {
const [, setOpen] = modalState
const [value, setValue] = useState(editingStat.value.toString())
const [isLoading, setLoading] = useState(false)
Expand All @@ -34,7 +32,7 @@ export default function UpdateStatValue({ modalState, mutate, editingStat, playe
setError(null)

try {
const { playerStat } = await updateStatValue(activeGame.id, player.id, editingStat.id, Number(value))
const { playerStat } = await updateStatValue(activeGame.id, editingStat.stat.id, editingStat.id, Number(value))
mutate((data) => {
return {
...data,
Expand Down
26 changes: 17 additions & 9 deletions src/pages/LeaderboardEntries.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import findLeaderboard from '../api/findLeaderboard'
import { LeaderboardEntry } from '../entities/leaderboardEntry'
import { Leaderboard } from '../entities/leaderboard'
import { Prop } from '../entities/prop'
import canPerformAction, { PermissionBasedAction } from '../utils/canPerformAction'
import userState, { AuthedUser } from '../state/userState'

function LeaderboardEntryProps({ props }: { props: Prop[] }) {
return props.map(({ key, value }) => (
Expand Down Expand Up @@ -49,6 +51,8 @@ export default function LeaderboardEntries() {

const navigate = useNavigate()

const user = useRecoilValue(userState) as AuthedUser

useEffect(() => {
(async () => {
if (isLoading) {
Expand Down Expand Up @@ -105,6 +109,8 @@ export default function LeaderboardEntries() {
)
}

const canUpdateEntry = canPerformAction(user, PermissionBasedAction.UPDATE_LEADERBOARD_ENTRY)

return (
<Page
showBackButton
Expand All @@ -119,7 +125,7 @@ export default function LeaderboardEntries() {

{!fetchError && entries.length > 0 &&
<>
<Table columns={['#', 'Player', 'Score', 'Props', 'Submitted at', '']}>
<Table columns={['#', 'Player', 'Score', 'Props', 'Submitted at', ...(canUpdateEntry ? [''] : [])]}>
<TableBody
iterator={entries}
configureClassnames={(entry, idx) => ({
Expand Down Expand Up @@ -148,14 +154,16 @@ export default function LeaderboardEntries() {
</div>
</TableCell>
<DateCell>{format(new Date(entry.createdAt), 'dd MMM Y, HH:mm')}</DateCell>
<TableCell className='w-40'>
<Button
variant={entry.hidden ? 'black' : 'grey'}
onClick={() => onHideToggle(entry)}
>
<span>{entry.hidden ? 'Unhide' : 'Hide'}</span>
</Button>
</TableCell>
{canUpdateEntry &&
<TableCell className='w-40'>
<Button
variant={entry.hidden ? 'black' : 'grey'}
onClick={() => onHideToggle(entry)}
>
<span>{entry.hidden ? 'Unhide' : 'Hide'}</span>
</Button>
</TableCell>
}
</>
)}
</TableBody>
Expand Down
37 changes: 34 additions & 3 deletions src/pages/PlayerLeaderboardEntries.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,28 @@ import activeGameState, { SelectedActiveGame } from '../state/activeGameState'
import useLeaderboards from '../api/useLeaderboards'
import usePlayerLeaderboardEntries from '../api/usePlayerLeaderboardEntries'
import Button from '../components/Button'
import { IconArrowRight } from '@tabler/icons-react'
import { IconArrowRight, IconPencil } from '@tabler/icons-react'
import { useNavigate } from 'react-router-dom'
import routes from '../constants/routes'
import PlayerAliases from '../components/PlayerAliases'
import { PermissionBasedAction } from '../utils/canPerformAction'
import canPerformAction from '../utils/canPerformAction'
import userState from '../state/userState'
import { AuthedUser } from '../state/userState'
import { useState } from 'react'
import { LeaderboardEntry } from '../entities/leaderboardEntry'
import UpdateEntryScore from '../modals/UpdateEntryScore'

export default function PlayerLeaderboardEntries() {
const user = useRecoilValue(userState) as AuthedUser
const activeGame = useRecoilValue(activeGameState) as SelectedActiveGame
const [editingEntry, setEditingEntry] = useState<LeaderboardEntry | null>(null)

const [player] = usePlayer()
const navigate = useNavigate()

const { leaderboards, loading: leaderboardsLoading, error: leaderboardsError } = useLeaderboards(activeGame)
const { entries, loading: entriesLoading, error: entriesError } = usePlayerLeaderboardEntries(activeGame, leaderboards, player)
const { entries, loading: entriesLoading, error: entriesError, mutate } = usePlayerLeaderboardEntries(activeGame, leaderboards, player)

const error = leaderboardsError || entriesError
const loading = !player || leaderboardsLoading || entriesLoading
Expand Down Expand Up @@ -64,14 +73,36 @@ export default function PlayerLeaderboardEntries() {
</div>
</TableCell>
<TableCell className='min-w-60'><PlayerAliases aliases={[entry.playerAlias]} /></TableCell>
<TableCell>{entry.score}</TableCell>
<TableCell className='flex items-center space-x-2'>
<span>{entry.score}</span>
{canPerformAction(user, PermissionBasedAction.UPDATE_LEADERBOARD_ENTRY) &&
<Button
variant='icon'
className='p-1 rounded-full bg-indigo-900'
onClick={() => setEditingEntry(entry)}
icon={<IconPencil size={16} />}
extra={{ 'aria-label': 'Edit leaderboard entry' }}
/>
}
</TableCell>
<DateCell>{format(new Date(entry.createdAt), 'dd MMM Y, HH:mm')}</DateCell>
</>
)}
</TableBody>
</Table>
}

{editingEntry &&
<UpdateEntryScore
modalState={[true, () => setEditingEntry(null)]}
mutate={mutate}
editingEntry={editingEntry}
leaderboard={leaderboards.find((leaderboard) => {
return leaderboard.internalName === editingEntry.leaderboardInternalName
})!}
/>
}

{error && <ErrorMessage error={error} />}
</Page>
)
Expand Down
1 change: 0 additions & 1 deletion src/pages/PlayerStats.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,6 @@ export default function PlayerStats() {
modalState={[true, () => setEditingStat(null)]}
mutate={mutate}
editingStat={editingStat}
player={player}
/>
}

Expand Down
4 changes: 3 additions & 1 deletion src/utils/canPerformAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ export enum PermissionBasedAction {
DELETE_GROUP,
DELETE_FEEDBACK_CATEGORY,
VIEW_PLAYER_AUTH_ACTIVITIES,
UPDATE_PLAYER_STAT
UPDATE_PLAYER_STAT,
UPDATE_LEADERBOARD_ENTRY
}

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

0 comments on commit a0ad8ce

Please sign in to comment.