Skip to content

Commit 53debd7

Browse files
authored
Merge pull request #392 from TaloDev/develop
Release 0.56.0
2 parents e12e99b + 23c468c commit 53debd7

File tree

13 files changed

+193
-80
lines changed

13 files changed

+193
-80
lines changed

README.md

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
# Talo frontend: self-hostable game dev dashboard
22

3-
Talo is a collection of tools and APIs designed to make game development easier and to help you make better data-driven decisions.
4-
5-
The Talo dashboard gives you a visual overview of your game including players, leaderboards, game save data and various analytics.
3+
Talo is the easiest way to add leaderboards, player authentication, socket-based multiplayer and more to your game. Using the Talo Dashboard, you can visualise and analyse your game data to make data-driven decisions.
64

75
Talo is available to use via our [Godot plugin](https://github.com/TaloDev/godot), [Unity package](https://github.com/TaloDev/unity) or [REST API](https://docs.trytalo.com/docs/http/authentication).
86

9-
## Talo's key features
7+
## Features
108

119
- 👥 [Player management](https://trytalo.com/players): Persist player data across sessions, create segments and handle authentication.
1210
- ⚡️ [Event tracking](https://trytalo.com/events): Track in-game player actions individually and globally.
@@ -22,22 +20,16 @@ Talo is available to use via our [Godot plugin](https://github.com/TaloDev/godot
2220
- 🗣️ [Game feedback](https://trytalo.com/feedback): Collect and manage feedback from your players.
2321
- 🔔 [Player presence](https://trytalo.com/players#presence): See if players are online and set custom statuses.
2422

25-
## Documentation
26-
27-
Check out the [full Talo docs](https://docs.trytalo.com) for setup instructions, detailed API docs/examples and configuration options.
28-
29-
## Self-hosting your own Talo instance
23+
## Links
3024

31-
Talo is designed to be easily self-hosted. Take a look at our [self-hosting guide](https://docs.trytalo.com/docs/selfhosting/overview) and the [GitHub repo](https://github.com/TaloDev/hosting) for examples on how to get started.
25+
- [Website](https://trytalo.com)
26+
- [Documentation](https://docs.trytalo.com)
27+
- [Self-hosting examples](https://github.com/talodev/hosting)
3228

33-
## Contributing to Talo
29+
## Contributing
3430

3531
Thinking about contributing to Talo? We’d love the help! Head over to our [contribution guide](CONTRIBUTING.md) to learn how to set up the project, run tests, and start adding new features.
3632

37-
## Join our community
33+
## Join the community
3834

3935
Have questions, want to share feedback or show off your game? [Join us on Discord](https://trytalo.com/discord) to connect with other developers and get help from the Talo team.
40-
41-
---
42-
43-
Find all the details about Talo on our [website](https://trytalo.com)!

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
@@ -79,7 +79,7 @@
7979
"lint-staged": {
8080
"*.{ts,js,tsx,jsx}": "eslint --fix"
8181
},
82-
"version": "0.55.0",
82+
"version": "0.56.0",
8383
"engines": {
8484
"node": "20.x"
8585
},

src/components/Pagination.tsx

Lines changed: 84 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,40 +6,107 @@ type PaginationProps = {
66
count: number
77
pageState: [number, Dispatch<SetStateAction<number>>]
88
itemsPerPage: number
9+
maxPageButtons?: number
910
}
1011

1112
export default function Pagination({
1213
count,
1314
pageState,
14-
itemsPerPage
15+
itemsPerPage,
16+
maxPageButtons = 10
1517
}: PaginationProps) {
18+
const [page, setPage] = pageState
1619
const totalPages = Math.ceil(count / itemsPerPage)
1720

18-
if (totalPages === 1) return null
21+
if (totalPages <= 1) {
22+
return null
23+
}
1924

20-
const [page, setPage] = pageState
21-
const pages = [...new Array(totalPages)].map((_, idx) => String(idx + 1))
25+
let startPage = Math.max(0, page - Math.floor(maxPageButtons / 2))
26+
const endPage = Math.min(totalPages - 1, startPage + maxPageButtons - 1)
27+
28+
if (endPage - startPage + 1 < maxPageButtons) {
29+
startPage = Math.max(0, endPage - maxPageButtons + 1)
30+
}
31+
32+
const pagesToShow: (number | 'ellipsis')[] = []
33+
34+
if (startPage > 0) {
35+
pagesToShow.push(0)
36+
if (startPage > 1) {
37+
pagesToShow.push('ellipsis')
38+
}
39+
}
40+
41+
for (let i = startPage; i <= endPage; i++) {
42+
pagesToShow.push(i)
43+
}
44+
45+
if (endPage < totalPages - 1) {
46+
if (endPage < totalPages - 2) {
47+
pagesToShow.push('ellipsis')
48+
}
49+
pagesToShow.push(totalPages - 1)
50+
}
51+
52+
const goToPage = (pageNumber: number) => {
53+
setPage(Math.max(0, Math.min(pageNumber, totalPages - 1)))
54+
}
2255

2356
return (
24-
<div className='w-full flex justify-center'>
25-
<ul className='flex -ml-1'>
26-
{pages.map((val, idx) => (
27-
<li key={val}>
57+
<div className='w-full flex justify-center py-4'>
58+
<nav aria-label='Pagination'>
59+
<ul className='flex items-center space-x-2'>
60+
<li>
2861
<Button
2962
variant='bare'
63+
className='px-3 py-2 text-sm font-medium text-black bg-white rounded-md hover:enabled:bg-gray-200 disabled:opacity-50 disabled:cursor-not-allowed'
64+
onClick={() => goToPage(page - 1)}
65+
disabled={page === 0}
66+
>
67+
Previous
68+
</Button>
69+
</li>
70+
71+
{pagesToShow.map((pageNumber, idx) => (
72+
<li
73+
key={idx}
3074
className={clsx(
31-
'py-2 w-8 ml-1 text-black text-center rounded-sm',
32-
{ 'bg-white': page !== idx },
33-
{
34-
'bg-indigo-500 !text-white': page === idx
35-
})}
36-
onClick={() => setPage(idx)}
75+
'lg:inline',
76+
{ 'hidden': typeof pageNumber !== 'number' || Math.abs(pageNumber - page) > 2 },
77+
{ 'md:inline': typeof pageNumber === 'number' && Math.abs(pageNumber - page) <= 4 }
78+
)}
79+
>
80+
{pageNumber === 'ellipsis' ? (
81+
<span className='p-2 text-sm font-medium text-gray-400'>...</span>
82+
) : (
83+
<Button
84+
variant='bare'
85+
className={clsx(
86+
'min-w-10 px-3 py-2 text-black text-center text-sm font-medium rounded-md',
87+
{ 'bg-white hover:bg-gray-200': page !== pageNumber },
88+
{ 'bg-indigo-500 text-white': page === pageNumber }
89+
)}
90+
onClick={() => goToPage(pageNumber as number)}
91+
>
92+
{String((pageNumber as number) + 1)}
93+
</Button>
94+
)}
95+
</li>
96+
))}
97+
98+
<li>
99+
<Button
100+
variant='bare'
101+
className='px-3 py-2 text-sm font-medium text-black bg-white rounded-md hover:enabled:bg-gray-200 disabled:opacity-50 disabled:cursor-not-allowed'
102+
onClick={() => goToPage(page + 1)}
103+
disabled={page === totalPages - 1}
37104
>
38-
{val}
105+
Next
39106
</Button>
40107
</li>
41-
))}
42-
</ul>
108+
</ul>
109+
</nav>
43110
</div>
44111
)
45112
}

src/components/PlayerAliases.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export function SingleAlias({ alias }: { alias: PlayerAlias }) {
3232
>
3333
<span
3434
className={clsx('p-1 rounded-full bg-gray-900 text-white', {
35-
'text-green-500': alias.player.presence?.online
35+
'!text-green-400': alias.player.presence?.online
3636
})}
3737
>
3838
{getIcon(alias)}

src/components/PropBadges.tsx

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { ReactElement, useMemo } from 'react'
2+
import { isMetaProp } from '../constants/metaProps'
3+
import { Prop } from '../entities/prop'
4+
import clsx from 'clsx'
5+
import Tippy from '@tippyjs/react'
6+
import { focusStyle } from '../styles/theme'
7+
8+
type Props = {
9+
props: Prop[]
10+
icon?: ReactElement
11+
onClick?: (prop: Prop) => void
12+
devBuild?: boolean
13+
buttonTitle?: string
14+
className?: string
15+
}
16+
17+
export function PropBadges({ props, icon: Icon, devBuild, onClick, buttonTitle, className }: Props) {
18+
const sortedProps = useMemo(() => {
19+
return props
20+
.filter((prop) => !isMetaProp(prop))
21+
.sort((a, b) => a.key.localeCompare(b.key))
22+
}, [props])
23+
24+
return (
25+
<div className={clsx('space-y-2', className)}>
26+
{sortedProps.map(({ key, value }) => (
27+
<span
28+
key={`${key}-${value}`}
29+
className='bg-gray-900 rounded text-xs flex w-fit'
30+
>
31+
<code className='align-middle inline-block p-2 break-all'>{key} = {value}</code>
32+
{onClick && (
33+
<Tippy content={<p>{buttonTitle}</p>}>
34+
<button
35+
type='button'
36+
className={clsx('grow px-2 bg-indigo-900 rounded-r', focusStyle, { 'bg-orange-900': devBuild })}
37+
onClick={() => onClick({ key, value })}
38+
aria-label={buttonTitle}
39+
>
40+
{Icon}
41+
</button>
42+
</Tippy>
43+
)}
44+
</span>
45+
))}
46+
</div>
47+
)
48+
}

src/components/__tests__/Pagination.test.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,14 @@ import Pagination from '../Pagination'
55
describe('<Pagination />', () => {
66
it('should render the correct amount of pages', () => {
77
render(<Pagination count={50} pageState={[0, vi.fn()]} itemsPerPage={25} />)
8-
expect(screen.getAllByRole('listitem')).toHaveLength(2)
8+
// previous, 1, 2, next
9+
expect(screen.getAllByRole('listitem')).toHaveLength(4)
10+
})
11+
12+
it('should render the correct amount of pages and ellipsis', () => {
13+
render(<Pagination count={350} pageState={[7, vi.fn()]} itemsPerPage={25} maxPageButtons={10} />)
14+
// previous, 1, ..., 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, ..., 14, next
15+
expect(screen.getAllByRole('listitem')).toHaveLength(16)
916
})
1017

1118
it('should go to the correct page on click', async () => {

src/entities/gameFeedback.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { z } from 'zod'
22
import { playerAliasSchema } from './playerAlias'
33
import { gameFeedbackCategorySchema } from './gameFeedbackCategory'
4+
import { propSchema } from './prop'
45

56
export const gameFeedbackSchema = z.object({
67
id: z.number(),
@@ -9,6 +10,7 @@ export const gameFeedbackSchema = z.object({
910
anonymised: z.boolean(),
1011
playerAlias: playerAliasSchema.nullable(),
1112
devBuild: z.boolean(),
13+
props: z.array(propSchema),
1214
createdAt: z.string().datetime()
1315
})
1416

src/pages/Feedback.tsx

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,16 @@ import { useNavigate } from 'react-router-dom'
1414
import routes from '../constants/routes'
1515
import useFeedback from '../api/useFeedback'
1616
import useFeedbackCategories from '../api/useFeedbackCategories'
17-
import { IconArrowRight } from '@tabler/icons-react'
17+
import { IconArrowRight, IconCrosshair } from '@tabler/icons-react'
1818
import clsx from 'clsx'
19-
import { useState } from 'react'
19+
import { useCallback, useState } from 'react'
2020
import canViewPage from '../utils/canViewPage'
2121
import userState from '../state/userState'
2222
import Pagination from '../components/Pagination'
2323
import TextInput from '../components/TextInput'
2424
import useSearch from '../utils/useSearch'
25+
import { PropBadges } from '../components/PropBadges'
26+
import { Prop } from '../entities/prop'
2527

2628
export default function Feedback() {
2729
const user = useRecoilValue(userState)
@@ -43,9 +45,14 @@ export default function Feedback() {
4345

4446
const navigate = useNavigate()
4547

46-
const goToPlayer = (identifier: string) => {
48+
const goToPlayer = useCallback((identifier: string) => {
4749
navigate(`${routes.players}?search=${identifier}`)
48-
}
50+
}, [navigate])
51+
52+
const setPropSearch = useCallback((prop: Prop) => {
53+
window.scrollTo(0, 0)
54+
setSearch(`prop:${prop.key}=${prop.value}`)
55+
}, [setSearch])
4956

5057
return (
5158
<Page
@@ -82,7 +89,7 @@ export default function Feedback() {
8289
<TextInput
8390
id='feedback-search'
8491
type='search'
85-
placeholder='Search...'
92+
placeholder='Search by comment, player or props...'
8693
onChange={setSearch}
8794
value={search}
8895
/>
@@ -106,7 +113,7 @@ export default function Feedback() {
106113

107114
{!feedbackError && sortedFeedback.length > 0 &&
108115
<>
109-
<Table columns={['Submitted at', 'Category', 'Comment', 'Player']}>
116+
<Table columns={['Submitted at', 'Category', 'Comment', 'Props', 'Player']}>
110117
<TableBody
111118
iterator={sortedFeedback}
112119
configureClassnames={(feedback, idx) => ({
@@ -118,7 +125,16 @@ export default function Feedback() {
118125
<>
119126
<DateCell>{format(new Date(feedback.createdAt), 'dd MMM Y, HH:mm')}</DateCell>
120127
<TableCell>{feedback.category.name}</TableCell>
121-
<TableCell className='min-w-[320px] max-w-[320px] whitespace-pre-wrap'>{feedback.comment}</TableCell>
128+
<TableCell className='w-[400px] whitespace-pre-wrap'>{feedback.comment}</TableCell>
129+
<TableCell className='w-[400px]'>
130+
<PropBadges
131+
props={feedback.props}
132+
devBuild={feedback.devBuild}
133+
icon={<IconCrosshair size={20} />}
134+
onClick={setPropSearch}
135+
buttonTitle='Filter by this prop'
136+
/>
137+
</TableCell>
122138
<TableCell>
123139
{feedback.playerAlias &&
124140
<div className='flex items-center'>

0 commit comments

Comments
 (0)