Skip to content

Commit 4d59a28

Browse files
authored
Merge pull request #437 from TaloDev/develop
Release 0.65.0
2 parents 9bf44be + 1684ce9 commit 4d59a28

File tree

10 files changed

+217
-11
lines changed

10 files changed

+217
-11
lines changed

package-lock.json

Lines changed: 2 additions & 2 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.64.2",
81+
"version": "0.65.0",
8282
"engines": {
8383
"node": "22.x"
8484
},

src/Router.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ const Channels = lazy(() => import(/* webpackChunkName: 'channels' */ './pages/C
4848
const EventBreakdown = lazy(() => import(/* webpackChunkName: 'event-breakdown' */ './pages/EventBreakdown'))
4949
const GameSettings = lazy(() => import(/* webpackChunkName: 'game-settings */ './pages/GameSettings'))
5050
const StatMetrics = lazy(() => import(/* webpackChunkName: 'stat-metrics */ './pages/StatMetrics'))
51+
const ChannelStorage = lazy(() => import(/* webpackChunkName: 'channel-storage */ './pages/ChannelStorage'))
5152

5253
type RouterProps = {
5354
intendedRoute: string | null
@@ -118,6 +119,7 @@ function Router({
118119
<Route path={routes.eventBreakdown} element={<EventBreakdown />} />
119120
{canViewPage(user, routes.gameSettings) && <Route path={routes.gameSettings} element={<GameSettings />} />}
120121
<Route path={routes.statMetrics} element={<StatMetrics />} />
122+
<Route path={routes.channelStorage} element={<ChannelStorage />} />
121123
</>
122124
}
123125

src/api/useChannelStorage.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import useSWR from 'swr'
2+
import buildError from '../utils/buildError'
3+
import { Game } from '../entities/game'
4+
import makeValidatedGetRequest from './makeValidatedGetRequest'
5+
import { z } from 'zod'
6+
import { channelStoragePropSchema } from '../entities/channelStorageProp'
7+
8+
export const channelStorageSchema = z.object({
9+
storageProps: z.array(channelStoragePropSchema),
10+
channelName: z.string(),
11+
count: z.number(),
12+
itemsPerPage: z.number()
13+
})
14+
15+
export default function useChannelStorage(activeGame: Game, channelId: number, search: string, page: number) {
16+
const fetcher = async ([url]: [string]) => {
17+
const params = new URLSearchParams({ page: String(page), search })
18+
const res = await makeValidatedGetRequest(`${url}?${params.toString()}`, channelStorageSchema)
19+
return res
20+
}
21+
22+
const { data, error } = useSWR(
23+
[`/games/${activeGame.id}/game-channels/${channelId}/storage`, search, page],
24+
fetcher
25+
)
26+
27+
return {
28+
storageProps: data?.storageProps ?? [],
29+
channelName: data?.channelName,
30+
count: data?.count,
31+
itemsPerPage: data?.itemsPerPage,
32+
loading: !data && !error,
33+
error: error && buildError(error),
34+
errorStatusCode: error && error.response?.status
35+
}
36+
}

src/components/ServicesLink.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ function ServicesLink() {
9090
},
9191
{
9292
name: 'Channels',
93-
desc: 'Manage player chat channels',
93+
desc: 'Manage player communication channels',
9494
icon: IconMessages,
9595
route: routes.channels
9696
}

src/constants/metaProps.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ const META_STEAMWORKS_PUBLISHER_BANNED = 'META_STEAMWORKS_PUBLISHER_BANNED'
1313
const META_STEAMWORKS_OWNS_APP = 'META_STEAMWORKS_OWNS_APP'
1414
const META_STEAMWORKS_OWNS_APP_PERMANENTLY = 'META_STEAMWORKS_OWNS_APP_PERMANENTLY'
1515
const META_STEAMWORKS_OWNS_APP_FROM_DATE = 'META_STEAMWORKS_OWNS_APP_FROM_DATE'
16+
const META_STEAMWORKS_AVATAR_HASH = 'META_STEAMWORKS_AVATAR_HASH'
17+
const META_STEAMWORKS_PERSONA_NAME = 'META_STEAMWORKS_PERSONA_NAME'
1618

1719
export const metaPropKeyMap = {
1820
[META_DEV_BUILD]: 'DEV BUILD',
@@ -25,7 +27,9 @@ export const metaPropKeyMap = {
2527
[META_STEAMWORKS_PUBLISHER_BANNED]: 'STEAMWORKS PUBLISHER BANNED',
2628
[META_STEAMWORKS_OWNS_APP]: 'STEAMWORKS OWNS APP',
2729
[META_STEAMWORKS_OWNS_APP_PERMANENTLY]: 'STEAMWORKS OWNS APP PERMANENTLY',
28-
[META_STEAMWORKS_OWNS_APP_FROM_DATE]: 'STEAMWORKS OWNS APP FROM DATE'
30+
[META_STEAMWORKS_OWNS_APP_FROM_DATE]: 'STEAMWORKS OWNS APP FROM DATE',
31+
[META_STEAMWORKS_AVATAR_HASH]: 'STEAMWORKS AVATAR HASH',
32+
[META_STEAMWORKS_PERSONA_NAME]: 'STEAM USERNAME'
2933
}
3034

3135
export function isMetaProp(prop: Prop) {

src/constants/routes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export default {
55
apiKeys: '/api-keys',
66
billing: '/billing',
77
channels: '/channels',
8+
channelStorage: '/channels/:channelId/storage',
89
confirmPassword: '/confirm-password',
910
dashboard: '/',
1011
dataExports: '/exports',

src/entities/channelStorageProp.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { z } from 'zod'
2+
import { playerAliasSchema } from './playerAlias'
3+
4+
export const channelStoragePropSchema = z.object({
5+
key: z.string(),
6+
value: z.string().nullable(),
7+
createdBy: playerAliasSchema,
8+
lastUpdatedBy: playerAliasSchema,
9+
createdAt: z.string(),
10+
updatedAt: z.string()
11+
})
12+
13+
export type ChannelStorageProp = z.infer<typeof channelStoragePropSchema>

src/pages/ChannelStorage.tsx

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import { format } from 'date-fns'
2+
import { useRecoilValue } from 'recoil'
3+
import Button from '../components/Button'
4+
import ErrorMessage from '../components/ErrorMessage'
5+
import Page from '../components/Page'
6+
import DateCell from '../components/tables/cells/DateCell'
7+
import Table from '../components/tables/Table'
8+
import TableBody from '../components/tables/TableBody'
9+
import TableCell from '../components/tables/TableCell'
10+
import activeGameState, { SelectedActiveGame } from '../state/activeGameState'
11+
import useSortedItems from '../utils/useSortedItems'
12+
import { useNavigate, useParams } from 'react-router-dom'
13+
import routes from '../constants/routes'
14+
import { IconArrowRight } from '@tabler/icons-react'
15+
import clsx from 'clsx'
16+
import Pagination from '../components/Pagination'
17+
import TextInput from '../components/TextInput'
18+
import useSearch from '../utils/useSearch'
19+
import useChannelStorage from '../api/useChannelStorage'
20+
import { useEffect } from 'react'
21+
22+
export default function ChannelStorage() {
23+
const { channelId } = useParams()
24+
const activeGame = useRecoilValue(activeGameState) as SelectedActiveGame
25+
26+
const { search, setSearch, page, setPage, debouncedSearch } = useSearch()
27+
28+
const {
29+
storageProps,
30+
channelName,
31+
loading,
32+
count,
33+
itemsPerPage,
34+
error,
35+
errorStatusCode
36+
} = useChannelStorage(activeGame, Number(channelId!), debouncedSearch, page)
37+
38+
const sortedProps = useSortedItems(storageProps, 'updatedAt')
39+
40+
const navigate = useNavigate()
41+
42+
const goToPlayer = (identifier: string) => {
43+
navigate(`${routes.players}?search=${identifier}`)
44+
}
45+
46+
const channelNotFound = errorStatusCode === 404
47+
48+
useEffect(() => {
49+
if (channelNotFound) {
50+
navigate(routes.channels, { replace: true })
51+
}
52+
}, [channelNotFound, navigate])
53+
54+
if (channelNotFound) {
55+
return null
56+
}
57+
58+
return (
59+
<Page
60+
showBackButton
61+
title={channelName ? `${channelName} storage` : 'Channel storage'}
62+
isLoading={loading}
63+
>
64+
{(sortedProps.length > 0 || debouncedSearch.length > 0) &&
65+
<div className='flex items-center'>
66+
<div className='w-1/2 grow md:grow-0 md:w-[400px]'>
67+
<TextInput
68+
id='storage-search'
69+
type='search'
70+
placeholder='Search...'
71+
onChange={setSearch}
72+
value={search}
73+
/>
74+
</div>
75+
{Boolean(count) && <span className='ml-4'>{count} storage {count === 1 ? 'prop' : 'props'}</span>}
76+
</div>
77+
}
78+
79+
{!error && !loading && storageProps.length === 0 &&
80+
<>
81+
{debouncedSearch.length > 0 &&
82+
<p>No storage props match your query</p>
83+
}
84+
{debouncedSearch.length === 0 && channelName &&
85+
<p>{channelName} doesn&apos;t have any storage props yet</p>
86+
}
87+
</>
88+
}
89+
90+
{!error && storageProps.length > 0 &&
91+
<>
92+
<Table columns={['Key', 'Value', 'Created by', 'Last updated by', 'Created at', 'Updated at']}>
93+
<TableBody iterator={sortedProps}>
94+
{(storageProp) => (
95+
<>
96+
<TableCell>{storageProp.key}</TableCell>
97+
<TableCell>{storageProp.value}</TableCell>
98+
<TableCell>
99+
<div className='flex items-center'>
100+
<span>{storageProp.createdBy.identifier}</span>
101+
<Button
102+
variant='icon'
103+
className={clsx('ml-2 p-1 rounded-full bg-indigo-900')}
104+
onClick={() => goToPlayer(storageProp.createdBy.identifier)}
105+
icon={<IconArrowRight size={16} />}
106+
/>
107+
</div>
108+
</TableCell>
109+
<TableCell>
110+
<div className='flex items-center'>
111+
<span>{storageProp.lastUpdatedBy.identifier}</span>
112+
<Button
113+
variant='icon'
114+
className={clsx('ml-2 p-1 rounded-full bg-indigo-900')}
115+
onClick={() => goToPlayer(storageProp.lastUpdatedBy.identifier)}
116+
icon={<IconArrowRight size={16} />}
117+
/>
118+
</div>
119+
</TableCell>
120+
<DateCell>{format(new Date(storageProp.createdAt), 'dd MMM yyyy, HH:mm')}</DateCell>
121+
<DateCell>{format(new Date(storageProp.updatedAt), 'dd MMM yyyy, HH:mm')}</DateCell>
122+
</>
123+
)}
124+
</TableBody>
125+
</Table>
126+
127+
{count && itemsPerPage && (
128+
<Pagination count={count} pageState={[page, setPage]} itemsPerPage={itemsPerPage} />
129+
)}
130+
</>
131+
}
132+
133+
{error && <ErrorMessage error={error} />}
134+
</Page>
135+
)
136+
}

src/pages/Channels.tsx

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export default function Channels() {
3535
mutate
3636
} = useChannels(activeGame, debouncedSearch, page)
3737

38-
const sortedChannels = useSortedItems(channels, 'memberCount')
38+
const sortedChannels = useSortedItems(channels, 'name')
3939

4040
const [showModal, setShowModal] = useState(false)
4141
const [editingChannel, setEditingChannel] = useState<GameChannel | null>(null)
@@ -59,6 +59,10 @@ export default function Channels() {
5959
setShowModal(true)
6060
}
6161

62+
const goToChannelStorage = (channel: GameChannel) => {
63+
navigate(routes.channelStorage.replace(':channelId', String(channel.id)))
64+
}
65+
6266
return (
6367
<Page
6468
title='Channels'
@@ -74,9 +78,9 @@ export default function Channels() {
7478
</div>
7579
}
7680
>
77-
{channels.length > 0 &&
81+
{(channels.length > 0 || debouncedSearch.length > 0) &&
7882
<div className='flex items-center'>
79-
<div className='w-1/2 flex-grow md:flex-grow-0 md:w-[400px]'>
83+
<div className='w-1/2 grow md:grow-0 md:w-[400px]'>
8084
<TextInput
8185
id='channel-search'
8286
type='search'
@@ -121,7 +125,17 @@ export default function Channels() {
121125
}
122126
{!channel.owner && 'Game-owned'}
123127
</TableCell>
124-
<TableCell className='font-mono'>{channel.memberCount.toLocaleString()}</TableCell>
128+
<TableCell className='font-mono'>
129+
<div className='flex items-center'>
130+
<span>{channel.memberCount.toLocaleString()}</span>
131+
<Button
132+
variant='icon'
133+
className={clsx('ml-2 p-1 rounded-full bg-indigo-900')}
134+
onClick={() => goToPlayersForChannel(channel)}
135+
icon={<IconArrowRight size={16} />}
136+
/>
137+
</div>
138+
</TableCell>
125139
<TableCell className='font-mono'>{channel.totalMessages.toLocaleString()}</TableCell>
126140
<DateCell>{format(new Date(channel.createdAt), 'dd MMM yyyy, HH:mm')}</DateCell>
127141
<DateCell>{format(new Date(channel.updatedAt), 'dd MMM yyyy, HH:mm')}</DateCell>
@@ -136,9 +150,9 @@ export default function Channels() {
136150
<TableCell className='w-48'>
137151
<Button
138152
variant='grey'
139-
onClick={() => goToPlayersForChannel(channel)}
153+
onClick={() => goToChannelStorage(channel)}
140154
>
141-
View players
155+
View storage
142156
</Button>
143157
</TableCell>
144158
</>

0 commit comments

Comments
 (0)