Skip to content

Commit

Permalink
show player online statuses plus new headlines
Browse files Browse the repository at this point in the history
  • Loading branch information
tudddorrr committed Feb 13, 2025
1 parent 2624b65 commit 0ca1796
Show file tree
Hide file tree
Showing 10 changed files with 103 additions and 10 deletions.
1 change: 1 addition & 0 deletions src/__mocks__/playerMock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export default function playerMock(extra: Partial<Player> = {}): Player {
lastSeenAt: new Date().toISOString(),
aliases: [],
groups: [],
presence: null,
...extra
}
}
36 changes: 36 additions & 0 deletions src/api/usePlayerHeadlines.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import useSWR from 'swr'
import buildError from '../utils/buildError'
import { Game } from '../entities/game'
import makeValidatedGetRequest from './makeValidatedGetRequest'
import { z } from 'zod'
import { PlayerHeadlines } from '../entities/playerHeadline'

const defaultHeadlines: PlayerHeadlines = {
total_players: { count: 0 },
online_players: { count: 0 }
}

export default function usePlayerHeadlines(activeGame: Game | null, includeDevData: boolean) {
const fetcher = async ([url]: [string]) => {
const headlines: (keyof PlayerHeadlines)[] = ['total_players', 'online_players']
const res = await Promise.all(headlines.map((headline) => makeValidatedGetRequest(`${url}/${headline}`, z.object({
count: z.number()
}))))

return headlines.reduce((acc, curr, idx) => ({
...acc,
[curr]: res[idx]
}), defaultHeadlines)
}

const { data, error } = useSWR(
activeGame ? [`/games/${activeGame.id}/headlines`, includeDevData] : null,
fetcher
)

return {
headlines: data ?? defaultHeadlines,
loading: !data && !error,
error: error && buildError(error)
}
}
2 changes: 1 addition & 1 deletion src/components/HeadlineStat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const HeadlineStat = (props: HeadlineStatProps) => {
</div>

<div className='p-4'>
<p className='text-4xl md:text-6xl'>{props.stat}</p>
<p className='text-4xl md:text-6xl'>{props.stat.toLocaleString()}</p>
</div>
</div>
)
Expand Down
10 changes: 8 additions & 2 deletions src/components/PlayerAliases.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,17 @@ export default function PlayerAliases({
<Tippy
content={
<p className={clsx({ 'capitalize': !alias.service.includes('_') && !alias.service.includes('-') })}>
{alias.service}
{alias.service}{alias.player.presence?.online && ' (Online)'}
</p>
}
>
<span className='p-1 rounded-full bg-gray-900'>{getIcon(alias)}</span>
<span
className={clsx('p-1 rounded-full bg-gray-900', {
'text-green-500': alias.player.presence?.online
})}
>
{getIcon(alias)}
</span>
</Tippy>

<span className='ml-2 text-sm'>{alias.identifier}</span>
Expand Down
4 changes: 3 additions & 1 deletion src/entities/player.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { z } from 'zod'
import { propSchema } from './prop'
import { playerAliasSchema, PlayerAlias } from './playerAlias'
import { playerPresenceSchema } from './playerPresence'

export const basePlayerSchema = z.object({
id: z.string().uuid(),
Expand All @@ -11,7 +12,8 @@ export const basePlayerSchema = z.object({
groups: z.array(z.object({
id: z.string(),
name: z.string()
}))
})),
presence: playerPresenceSchema.nullable()
})

export type Player = z.infer<typeof basePlayerSchema> & {
Expand Down
12 changes: 12 additions & 0 deletions src/entities/playerHeadline.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { z } from 'zod'

const countSchema = z.object({
count: z.number()
})

export const playerHeadlinesSchema = z.object({
total_players: countSchema,
online_players: countSchema
})

export type PlayerHeadlines = z.infer<typeof playerHeadlinesSchema>
9 changes: 9 additions & 0 deletions src/entities/playerPresence.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { z } from 'zod'

export const playerPresenceSchema = z.object({
online: z.boolean(),
customStatus: z.string(),
updatedAt: z.string().datetime()
})

export type PlayerPresence = z.infer<typeof playerPresenceSchema>
16 changes: 15 additions & 1 deletion src/pages/Dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import DevDataStatus from '../components/DevDataStatus'
import SecondaryTitle from '../components/SecondaryTitle'
import useIntendedRoute from '../utils/useIntendedRoute'
import usePinnedGroups from '../api/usePinnedGroups'
import usePlayerHeadlines from '../api/usePlayerHeadlines'

export const secondaryNavRoutes = [
{ title: 'Dashboard', to: routes.dashboard },
Expand Down Expand Up @@ -46,7 +47,7 @@ export default function Dashboard() {
const { headlines, loading: headlinesLoading, error: headlinesError } = useHeadlines(activeGame, startDate, endDate, includeDevData)
const { stats, loading: statsLoading, error: statsError } = useStats(activeGame, includeDevData)
const { groups: pinnedGroups, loading: pinnedGroupsLoading, error: pinnedGroupsError } = usePinnedGroups(activeGame, includeDevData)

const { headlines: playerHeadlines, loading: playerHeadlinesLoading, error: playerHeadlinesError } = usePlayerHeadlines(activeGame, includeDevData)
const intendedRouteChecked = useIntendedRoute()

if (!intendedRouteChecked) return null
Expand Down Expand Up @@ -91,6 +92,19 @@ export default function Dashboard() {
</div>
}

<SecondaryTitle>Players</SecondaryTitle>

{playerHeadlinesError &&
<ErrorMessage error={{ message: 'Couldn\'t fetch player headlines' }} />
}

{!playerHeadlinesLoading && !playerHeadlinesError &&
<div className='grid md:grid-cols-2 lg:grid-cols-4 gap-4'>
<HeadlineStat title='Total players' stat={playerHeadlines.total_players.count} />
<HeadlineStat title='Online players' stat={playerHeadlines.online_players.count} />
</div>
}

{pinnedGroups.length > 0 && <SecondaryTitle>Pinned groups</SecondaryTitle>}

{pinnedGroupsError &&
Expand Down
4 changes: 4 additions & 0 deletions src/pages/PlayerProfile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import activeGameState, { SelectedActiveGame } from '../state/activeGameState'
import useDaySections from '../utils/useDaySections'
import ActivityRenderer from '../components/ActivityRenderer'
import userState, { AuthedUser } from '../state/userState'
import clsx from 'clsx'

const links = [
{
Expand Down Expand Up @@ -87,6 +88,9 @@ export default function PlayerProfile() {
<div className='flex mt-4 space-x-2'>
<Identifier id={`Registered ${format(new Date(player.createdAt), 'do MMM Y')}`} />
<Identifier id={`Last seen ${format(new Date(player.lastSeenAt), 'do MMM Y')}`} />
<span className={clsx(player.presence?.online && 'text-green-500')}>
<Identifier id={`${player.presence?.online ? 'Online' : 'Offline'}${player.presence?.customStatus ? ` (${player.presence.customStatus})` : ''}`} />
</span>
</div>
</div>

Expand Down
19 changes: 14 additions & 5 deletions src/pages/__tests__/Dashboard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ describe('<Dashboard />', () => {
axiosMock.onGet(/\/games\/\d\/headlines\/unique_event_submitters/).replyOnce(200, {
count: 8
})
axiosMock.onGet(/\/games\/\d\/headlines\/total_players/).replyOnce(200, {
count: 150030
})
axiosMock.onGet(/\/games\/\d\/headlines\/online_players/).replyOnce(200, {
count: 2094
})

axiosMock.onGet(/\/games\/\d\/player-groups\/pinned/).replyOnce(200, {
groups: [
Expand Down Expand Up @@ -79,12 +85,15 @@ describe('<Dashboard />', () => {
expect(await screen.findByText('Swerve City dashboard')).toBeInTheDocument()

expect(await screen.findByText('New players')).toBeInTheDocument()
expect(screen.getByText('Returning players')).toBeInTheDocument()
expect(screen.getByText('New events')).toBeInTheDocument()
expect(screen.getByText('Unique event submitters')).toBeInTheDocument()
expect(await screen.findByText('Returning players')).toBeInTheDocument()
expect(await screen.findByText('New events')).toBeInTheDocument()
expect(await screen.findByText('Unique event submitters')).toBeInTheDocument()

expect(await screen.findByText('Stat A')).toBeInTheDocument()
expect(screen.getByText('Stat B')).toBeInTheDocument()
expect(await screen.findByText('Stat B')).toBeInTheDocument()

expect(await screen.findByText('150,030')).toBeInTheDocument()
expect(await screen.findByText('2,094')).toBeInTheDocument()
})

it('should be able to change the time period for headlines', async () => {
Expand Down Expand Up @@ -120,7 +129,7 @@ describe('<Dashboard />', () => {

await userEvent.click(screen.getByText('This year'))

expect(await screen.findByText('2103')).toBeInTheDocument()
expect(await screen.findByText('2,103')).toBeInTheDocument()
})

it('should render headline, pinned group and stat errors', async () => {
Expand Down

0 comments on commit 0ca1796

Please sign in to comment.