From 742e1fbc22b6382b047d0d3116992051f86bcd5b Mon Sep 17 00:00:00 2001 From: tudor <7089284+tudddorrr@users.noreply.github.com> Date: Sun, 1 Mar 2026 13:41:28 +0000 Subject: [PATCH] add empty states for most pages --- src/components/Modal.tsx | 2 +- src/components/PropsEditor.tsx | 12 +- src/components/RadioGroup.tsx | 19 +++- src/components/ServicesLink.tsx | 4 +- src/components/empty-states/EmptyState.tsx | 104 ++++++++++++++++++ src/components/empty-states/NoChannels.tsx | 26 +++++ src/components/empty-states/NoEvents.tsx | 22 ++++ src/components/empty-states/NoFeedback.tsx | 27 +++++ src/components/empty-states/NoGroups.tsx | 27 +++++ .../empty-states/NoLeaderboards.tsx | 27 +++++ src/components/empty-states/NoLiveConfig.tsx | 26 +++++ src/components/empty-states/NoPlayers.tsx | 27 +++++ src/components/empty-states/NoStats.tsx | 27 +++++ src/components/events/EventsDisplay.tsx | 5 +- src/modals/DocsTypeSelection.tsx | 54 +++++++++ src/pages/Channels.tsx | 5 +- src/pages/Feedback.tsx | 5 +- src/pages/FeedbackCategories.tsx | 2 +- src/pages/GameProps.tsx | 3 +- src/pages/Groups.tsx | 13 +-- src/pages/Leaderboards.tsx | 5 +- src/pages/PlayerProps.tsx | 4 +- src/pages/Players.tsx | 9 +- src/pages/Stats.tsx | 5 +- 24 files changed, 413 insertions(+), 47 deletions(-) create mode 100644 src/components/empty-states/EmptyState.tsx create mode 100644 src/components/empty-states/NoChannels.tsx create mode 100644 src/components/empty-states/NoEvents.tsx create mode 100644 src/components/empty-states/NoFeedback.tsx create mode 100644 src/components/empty-states/NoGroups.tsx create mode 100644 src/components/empty-states/NoLeaderboards.tsx create mode 100644 src/components/empty-states/NoLiveConfig.tsx create mode 100644 src/components/empty-states/NoPlayers.tsx create mode 100644 src/components/empty-states/NoStats.tsx create mode 100644 src/modals/DocsTypeSelection.tsx diff --git a/src/components/Modal.tsx b/src/components/Modal.tsx index 95c765a1..35355117 100644 --- a/src/components/Modal.tsx +++ b/src/components/Modal.tsx @@ -49,7 +49,7 @@ export default function Modal({
Promise - noPropsMessage: string + noPropsMessage: ReactNode } type MetaProp = { @@ -152,9 +152,7 @@ export default function PropsEditor({ startingProps, onSave, noPropsMessage }: P )} - {existingProps.length + newProps.length === 0 && ( -

{noPropsMessage}. Click the button below to add one.

- )} + {existingProps.length + newProps.length === 0 && noPropsMessage} {existingProps.length + newProps.length > 0 && ( <> @@ -164,7 +162,7 @@ export default function PropsEditor({ startingProps, onSave, noPropsMessage }: P {(prop) => ( <> 0 })} + className={clsx('min-w-80', { 'rounded-bl-none!': newProps.length > 0 })} > {prop.key} @@ -177,7 +175,7 @@ export default function PropsEditor({ startingProps, onSave, noPropsMessage }: P value={prop.value ?? ''} /> - 0 })}> + 0 })}> + +
+ {showModal && ( + + )} + + ) +} + +export function EmptyState({ title, icon, children, learnMoreLink, docs }: Props) { + return ( +
+
+ {icon} +
+ {title} + {children} + +
+ ) +} diff --git a/src/components/empty-states/NoChannels.tsx b/src/components/empty-states/NoChannels.tsx new file mode 100644 index 00000000..4e9fde07 --- /dev/null +++ b/src/components/empty-states/NoChannels.tsx @@ -0,0 +1,26 @@ +import { IconMessages } from '@tabler/icons-react' +import { useRecoilValue } from 'recoil' +import activeGameState, { SelectedActiveGame } from '../../state/activeGameState' +import { EmptyState } from './EmptyState' + +export function NoChannels() { + const activeGame = useRecoilValue(activeGameState) as SelectedActiveGame + + return ( + } + learnMoreLink='https://trytalo.com/channels' + docs={{ + api: 'https://docs.trytalo.com/docs/http/game-channel-api', + godot: 'https://docs.trytalo.com/docs/godot/channels', + unity: 'https://docs.trytalo.com/docs/unity/channels', + }} + > +

+ Channels can be used for player chats, sending custom data and storing data in a shared + pool. Channels can be public, private, temporary or permanent. +

+
+ ) +} diff --git a/src/components/empty-states/NoEvents.tsx b/src/components/empty-states/NoEvents.tsx new file mode 100644 index 00000000..19fc921c --- /dev/null +++ b/src/components/empty-states/NoEvents.tsx @@ -0,0 +1,22 @@ +import { EmptyStateButtons, EmptyStateContent, EmptyStateTitle } from './EmptyState' + +export function NoEvents() { + return ( +
+ There are no events for this date range + + Event tracking is a simple way to understand your players and their actions. When you track + an event, you can see how often it happens, when it happens and drill-down into its details. + + +
+ ) +} diff --git a/src/components/empty-states/NoFeedback.tsx b/src/components/empty-states/NoFeedback.tsx new file mode 100644 index 00000000..d51d7020 --- /dev/null +++ b/src/components/empty-states/NoFeedback.tsx @@ -0,0 +1,27 @@ +import { IconBubbleText } from '@tabler/icons-react' +import { useRecoilValue } from 'recoil' +import activeGameState, { SelectedActiveGame } from '../../state/activeGameState' +import { EmptyState } from './EmptyState' + +export function NoFeedback() { + const activeGame = useRecoilValue(activeGameState) as SelectedActiveGame + + return ( + } + learnMoreLink='https://trytalo.com/feedback' + docs={{ + api: 'https://docs.trytalo.com/docs/http/game-feedback-api', + godot: 'https://docs.trytalo.com/docs/godot/feedback', + unity: 'https://docs.trytalo.com/docs/unity/feedback', + }} + > +

+ Feedback from players can be filtered and sorted by categories and custom metadata. Feedback + can be anonymous or linked to a player, so you can see exactly what happened leading up to + it. +

+
+ ) +} diff --git a/src/components/empty-states/NoGroups.tsx b/src/components/empty-states/NoGroups.tsx new file mode 100644 index 00000000..7b471f19 --- /dev/null +++ b/src/components/empty-states/NoGroups.tsx @@ -0,0 +1,27 @@ +import { IconSocial } from '@tabler/icons-react' +import { useRecoilValue } from 'recoil' +import activeGameState, { SelectedActiveGame } from '../../state/activeGameState' +import { EmptyState } from './EmptyState' + +export function NoGroups() { + const activeGame = useRecoilValue(activeGameState) as SelectedActiveGame + + return ( + } + learnMoreLink='https://trytalo.com/players#groups' + docs={{ + api: 'https://docs.trytalo.com/docs/http/player-group-api', + godot: 'https://docs.trytalo.com/docs/godot/groups', + unity: 'https://docs.trytalo.com/docs/unity/groups', + }} + > +

+ Groups let you sort players based on stats, leaderboards, sessions or props. These segments + live-update and can be queried in-game, allow you to trigger custom logic for different + players. +

+
+ ) +} diff --git a/src/components/empty-states/NoLeaderboards.tsx b/src/components/empty-states/NoLeaderboards.tsx new file mode 100644 index 00000000..e7f56f19 --- /dev/null +++ b/src/components/empty-states/NoLeaderboards.tsx @@ -0,0 +1,27 @@ +import { IconTrophy } from '@tabler/icons-react' +import { useRecoilValue } from 'recoil' +import activeGameState, { SelectedActiveGame } from '../../state/activeGameState' +import { EmptyState } from './EmptyState' + +export function NoLeaderboards() { + const activeGame = useRecoilValue(activeGameState) as SelectedActiveGame + + return ( + } + learnMoreLink='https://trytalo.com/leaderboards' + docs={{ + api: 'https://docs.trytalo.com/docs/http/leaderboard-api', + godot: 'https://docs.trytalo.com/docs/godot/leaderboards', + unity: 'https://docs.trytalo.com/docs/unity/leaderboards', + }} + > +

+ Talo's leaderboards are highly customisable. Along with tracking scores, you can attach + custom metadata, enable timed refreshes (e.g. daily leaderboards) and hide suspicious + entries. +

+
+ ) +} diff --git a/src/components/empty-states/NoLiveConfig.tsx b/src/components/empty-states/NoLiveConfig.tsx new file mode 100644 index 00000000..bd12b8fe --- /dev/null +++ b/src/components/empty-states/NoLiveConfig.tsx @@ -0,0 +1,26 @@ +import { useRecoilValue } from 'recoil' +import activeGameState, { SelectedActiveGame } from '../../state/activeGameState' +import { EmptyStateButtons, EmptyStateContent, EmptyStateTitle } from './EmptyState' + +export function NoLiveConfig() { + const activeGame = useRecoilValue(activeGameState) as SelectedActiveGame + + return ( + <> + {activeGame.name} has no custom props + + + Live config is made up of custom props. These can be queried in-game to trigger custom logic + - like for seasonal events. + + + ) +} diff --git a/src/components/empty-states/NoPlayers.tsx b/src/components/empty-states/NoPlayers.tsx new file mode 100644 index 00000000..5511be91 --- /dev/null +++ b/src/components/empty-states/NoPlayers.tsx @@ -0,0 +1,27 @@ +import { IconUser } from '@tabler/icons-react' +import { useRecoilValue } from 'recoil' +import activeGameState, { SelectedActiveGame } from '../../state/activeGameState' +import { EmptyState } from './EmptyState' + +export function NoPlayers() { + const activeGame = useRecoilValue(activeGameState) as SelectedActiveGame + + return ( + } + learnMoreLink='https://trytalo.com/players' + docs={{ + api: 'https://docs.trytalo.com/docs/http/player-api', + godot: 'https://docs.trytalo.com/docs/godot/identifying', + unity: 'https://docs.trytalo.com/docs/unity/identifying', + }} + > +

+ Players can be created with or without authentication - they can be completely anonymous or + linked to a third party service like Steam. Players can also have custom metadata attached + to them that persist across sessions. +

+
+ ) +} diff --git a/src/components/empty-states/NoStats.tsx b/src/components/empty-states/NoStats.tsx new file mode 100644 index 00000000..c6163522 --- /dev/null +++ b/src/components/empty-states/NoStats.tsx @@ -0,0 +1,27 @@ +import { IconChartBar } from '@tabler/icons-react' +import { useRecoilValue } from 'recoil' +import activeGameState, { SelectedActiveGame } from '../../state/activeGameState' +import { EmptyState } from './EmptyState' + +export function NoStats() { + const activeGame = useRecoilValue(activeGameState) as SelectedActiveGame + + return ( + } + learnMoreLink='https://trytalo.com/stats' + docs={{ + api: 'https://docs.trytalo.com/docs/http/game-stat-api', + godot: 'https://docs.trytalo.com/docs/godot/stats', + unity: 'https://docs.trytalo.com/docs/unity/stats', + }} + > +

+ Talo's stat tracking is a simple way to track player stats and aggregate them as global + stats. Stats aren't just for analytics - you can also show players how stats change over + time in-game too. +

+
+ ) +} diff --git a/src/components/events/EventsDisplay.tsx b/src/components/events/EventsDisplay.tsx index e36f2899..c52c3ea7 100644 --- a/src/components/events/EventsDisplay.tsx +++ b/src/components/events/EventsDisplay.tsx @@ -17,6 +17,7 @@ import routes from '../../constants/routes' import { getPersistentColor } from '../../utils/getPersistentColour' import { EventChartTooltip } from '../charts/EventChartTooltip' import { useYAxis } from '../charts/useYAxis' +import { NoEvents } from '../empty-states/NoEvents' import Link from '../Link' import { useEventsContext } from './EventsContext' @@ -68,9 +69,7 @@ export default function EventsDisplay({ return ( <> - {!loading && Object.keys(events ?? {}).length === 0 && ( -

There are no events for this date range

- )} + {!loading && Object.keys(events ?? {}).length === 0 && } {error?.hasKeys === false && } diff --git a/src/modals/DocsTypeSelection.tsx b/src/modals/DocsTypeSelection.tsx new file mode 100644 index 00000000..0824e8f0 --- /dev/null +++ b/src/modals/DocsTypeSelection.tsx @@ -0,0 +1,54 @@ +import { useState } from 'react' +import Button from '../components/Button' +import Modal from '../components/Modal' +import RadioGroup from '../components/RadioGroup' + +export type DocType = 'api' | 'godot' | 'unity' + +type Props = { + modalState: [boolean, (open: boolean) => void] + onSelected: (selectedType: DocType) => void +} + +export function DocsTypeSelection({ modalState, onSelected }: Props) { + const [selectedType, setSelectedType] = useState(null) + + return ( + +
+ +
+ +
+
+ +
+
+
+ ) +} diff --git a/src/pages/Channels.tsx b/src/pages/Channels.tsx index 6062434a..5a0b9fa2 100644 --- a/src/pages/Channels.tsx +++ b/src/pages/Channels.tsx @@ -6,6 +6,7 @@ import { useNavigate } from 'react-router-dom' import { useRecoilValue } from 'recoil' import useChannels from '../api/useChannels' import Button from '../components/Button' +import { NoChannels } from '../components/empty-states/NoChannels' import ErrorMessage from '../components/ErrorMessage' import Page from '../components/Page' import Pagination from '../components/Pagination' @@ -97,9 +98,7 @@ export default function Channels() { {!error && !loading && sortedChannels.length === 0 && ( <> {debouncedSearch.length > 0 &&

No channels match your query

} - {debouncedSearch.length === 0 && ( -

{activeGame.name} doesn't have any channels yet

- )} + {debouncedSearch.length === 0 && } )} diff --git a/src/pages/Feedback.tsx b/src/pages/Feedback.tsx index 7a0a099e..bbdd6015 100644 --- a/src/pages/Feedback.tsx +++ b/src/pages/Feedback.tsx @@ -7,6 +7,7 @@ import { useRecoilValue } from 'recoil' import useFeedback from '../api/useFeedback' import useFeedbackCategories from '../api/useFeedbackCategories' import Button from '../components/Button' +import { NoFeedback } from '../components/empty-states/NoFeedback' import ErrorMessage from '../components/ErrorMessage' import Page from '../components/Page' import Pagination from '../components/Pagination' @@ -117,9 +118,7 @@ export default function Feedback() { {!feedbackError && !feedbackLoading && sortedFeedback.length === 0 && ( <> - {!categoryInternalNameFilter && debouncedSearch.length === 0 && ( -

{activeGame.name} doesn't have any feedback yet

- )} + {!categoryInternalNameFilter && debouncedSearch.length === 0 && } {(categoryInternalNameFilter || debouncedSearch.length > 0) && (

No feedback matches your query

)} diff --git a/src/pages/FeedbackCategories.tsx b/src/pages/FeedbackCategories.tsx index a452f153..f1dde639 100644 --- a/src/pages/FeedbackCategories.tsx +++ b/src/pages/FeedbackCategories.tsx @@ -50,7 +50,7 @@ export default function FeedbackCategories() { } isLoading={loading} > -
+
Back to feedback
diff --git a/src/pages/GameProps.tsx b/src/pages/GameProps.tsx index 2ae7d0f0..6e4dc29d 100644 --- a/src/pages/GameProps.tsx +++ b/src/pages/GameProps.tsx @@ -1,6 +1,7 @@ import { useContext } from 'react' import { useRecoilState } from 'recoil' import updateGame from '../api/updateGame' +import { NoLiveConfig } from '../components/empty-states/NoLiveConfig' import Page from '../components/Page' import PropsEditor from '../components/PropsEditor' import ToastContext, { ToastType } from '../components/toast/ToastContext' @@ -26,7 +27,7 @@ export default function GameProps() { } /> ) diff --git a/src/pages/Groups.tsx b/src/pages/Groups.tsx index 12eb3c55..fb6fc025 100644 --- a/src/pages/Groups.tsx +++ b/src/pages/Groups.tsx @@ -8,6 +8,7 @@ import togglePinnedGroup from '../api/toggledPinnedGroup' import useGroups from '../api/useGroups' import usePinnedGroups from '../api/usePinnedGroups' import Button from '../components/Button' +import { NoGroups } from '../components/empty-states/NoGroups' import ErrorMessage from '../components/ErrorMessage' import Identifier from '../components/Identifier' import Page from '../components/Page' @@ -55,7 +56,9 @@ export default function Groups() { const toast = useContext(ToastContext) useEffect(() => { - if (!showModal) setEditingGroup(null) + if (!showModal) { + setEditingGroup(null) + } }, [showModal, editingGroup]) const onEditGroupClick = (group: PlayerGroup) => { @@ -130,13 +133,7 @@ export default function Groups() { )} {sortedGroups.length === 0 && !loading && ( - <> - {search.length > 0 ? ( -

No groups match your query

- ) : ( -

{activeGame.name} doesn't have any groups yet

- )} - + <>{search.length > 0 ?

No groups match your query

: } )} {error && } diff --git a/src/pages/Leaderboards.tsx b/src/pages/Leaderboards.tsx index adad2b62..3567cf15 100644 --- a/src/pages/Leaderboards.tsx +++ b/src/pages/Leaderboards.tsx @@ -6,6 +6,7 @@ import { useRecoilValue } from 'recoil' import useLeaderboards from '../api/useLeaderboards' import Button from '../components/Button' import { NewLeaderboardEntriesChart } from '../components/charts/NewLeaderboardEntriesChart' +import { NoLeaderboards } from '../components/empty-states/NoLeaderboards' import ErrorMessage from '../components/ErrorMessage' import Page from '../components/Page' import DateCell from '../components/tables/cells/DateCell' @@ -78,9 +79,7 @@ export default function Leaderboards() { > {leaderboards.length > 0 && } - {!error && !loading && leaderboards.length === 0 && ( -

{activeGame.name} doesn't have any leaderboards yet

- )} + {!error && !loading && leaderboards.length === 0 && } {!error && leaderboards.length > 0 && ( This player has no custom properties. Click the button below to add one.

+ } /> ) diff --git a/src/pages/Players.tsx b/src/pages/Players.tsx index d4da50c8..7852043b 100644 --- a/src/pages/Players.tsx +++ b/src/pages/Players.tsx @@ -4,6 +4,7 @@ import { useRecoilValue } from 'recoil' import usePlayers from '../api/usePlayers' import Button from '../components/Button' import { NewPlayersChart } from '../components/charts/NewPlayersChart' +import { NoPlayers } from '../components/empty-states/NoPlayers' import ErrorMessage from '../components/ErrorMessage' import Page from '../components/Page' import Pagination from '../components/Pagination' @@ -59,13 +60,7 @@ export default function Players() { )} {players.length === 0 && !loading && ( - <> - {debouncedSearch.length > 0 ? ( -

No players match your query

- ) : ( -

{activeGame.name} doesn't have any players yet

- )} - + <>{debouncedSearch.length > 0 ?

No players match your query

: } )} {error && } diff --git a/src/pages/Stats.tsx b/src/pages/Stats.tsx index d7680825..3e63e2a5 100644 --- a/src/pages/Stats.tsx +++ b/src/pages/Stats.tsx @@ -6,6 +6,7 @@ import { useRecoilValue } from 'recoil' import useStats from '../api/useStats' import Button from '../components/Button' import { StatsActivityChart } from '../components/charts/StatsActivityChart' +import { NoStats } from '../components/empty-states/NoStats' import ErrorMessage from '../components/ErrorMessage' import Page from '../components/Page' import DateCell from '../components/tables/cells/DateCell' @@ -76,9 +77,7 @@ export default function Stats() { > {sortedStats.length > 0 && } - {!error && !loading && sortedStats.length === 0 && ( -

{activeGame.name} doesn't have any stats yet

- )} + {!error && !loading && sortedStats.length === 0 && } {!error && sortedStats.length > 0 && (