├── .dockerignore
├── .env.development
├── .env.e2e
├── .env.production
├── .eslintrc.json
├── .github
├── FUNDING.yml
├── release.yml
└── workflows
│ ├── ci.yml
│ ├── create-release.yml
│ ├── docker.yml
│ └── tag.yml
├── .gitignore
├── .husky
├── .gitignore
└── pre-commit
├── CONTRIBUTING.md
├── Dockerfile
├── LICENSE
├── README.md
├── config
├── Caddyfile
└── entrypoint.sh
├── cypress.config.js
├── cypress
├── e2e
│ └── pages
│ │ ├── AcceptInvite.spec.cy.js
│ │ ├── Billing.spec.cy.js
│ │ ├── Demo.spec.cy.js
│ │ ├── ForgotPassword.spec.cy.js
│ │ ├── Login.spec.cy.js
│ │ ├── RecoverAccount.spec.cy.js
│ │ ├── Register.spec.cy.js
│ │ ├── ResetPassword.spec.cy.js
│ │ └── Verify2FA.spec.cy.js
├── fixtures
│ └── responses
│ │ ├── auth
│ │ ├── dev.json
│ │ ├── owner.json
│ │ └── register.json
│ │ ├── billing
│ │ ├── free-plan.json
│ │ ├── invoice.json
│ │ ├── plans.json
│ │ └── usage.json
│ │ └── game-stats
│ │ └── global.json
└── support
│ ├── commands.js
│ └── e2e.js
├── index.html
├── package-lock.json
├── package.json
├── postcss.config.js
├── public
├── favicon.ico
└── robots.txt
├── renovate.json
├── setup-tests.js
├── src
├── App.tsx
├── Router.tsx
├── __mocks__
│ ├── gameFeedbackCategoryMock.ts
│ ├── gameMock.ts
│ ├── gameStatMock.ts
│ ├── leaderboardMock.ts
│ ├── organisationMock.ts
│ ├── playerAliasMock.ts
│ ├── playerMock.ts
│ ├── pricingPlanMock.ts
│ └── userMock.ts
├── api
│ ├── api.ts
│ ├── changePassword.ts
│ ├── confirmEmail.ts
│ ├── confirmEnable2FA.ts
│ ├── confirmPlan.ts
│ ├── createAPIKey.ts
│ ├── createChannel.ts
│ ├── createCheckoutSession.ts
│ ├── createDataExport.ts
│ ├── createDemo.ts
│ ├── createFeedbackCategory.ts
│ ├── createGame.ts
│ ├── createGroup.ts
│ ├── createInvite.ts
│ ├── createLeaderboard.ts
│ ├── createPortalSession.ts
│ ├── createRecoveryCodes.ts
│ ├── createStat.ts
│ ├── deleteAPIKey.ts
│ ├── deleteChannel.ts
│ ├── deleteFeedbackCategory.ts
│ ├── deleteGroup.ts
│ ├── deleteLeaderboard.ts
│ ├── deleteStat.ts
│ ├── disable2FA.ts
│ ├── disableIntegration.ts
│ ├── enable2FA.ts
│ ├── enableIntegration.ts
│ ├── findLeaderboard.ts
│ ├── findPlayer.ts
│ ├── getInvite.ts
│ ├── login.ts
│ ├── logout.ts
│ ├── makeValidatedGetRequest.ts
│ ├── makeValidatedRequest.ts
│ ├── recoverAccount.ts
│ ├── refreshAccess.ts
│ ├── register.ts
│ ├── requestNewPassword.ts
│ ├── resetPassword.ts
│ ├── syncLeaderboards.ts
│ ├── syncStats.ts
│ ├── toggledPinnedGroup.ts
│ ├── updateAPIKey.ts
│ ├── updateChannel.ts
│ ├── updateFeedbackCategory.ts
│ ├── updateGame.ts
│ ├── updateGroup.ts
│ ├── updateIntegration.ts
│ ├── updateLeaderboard.ts
│ ├── updateLeaderboardEntry.ts
│ ├── updatePlayer.ts
│ ├── updateStat.ts
│ ├── updateStatValue.ts
│ ├── useAPIKeys.ts
│ ├── useChannels.ts
│ ├── useDataExportEntities.ts
│ ├── useDataExports.ts
│ ├── useEventBreakdown.ts
│ ├── useEvents.ts
│ ├── useFeedback.ts
│ ├── useFeedbackCategories.ts
│ ├── useGameActivities.ts
│ ├── useGameSettings.ts
│ ├── useGroupPreviewCount.ts
│ ├── useGroupRules.ts
│ ├── useGroups.ts
│ ├── useHeadlines.ts
│ ├── useIntegrations.ts
│ ├── useLeaderboardEntries.ts
│ ├── useLeaderboards.ts
│ ├── useOrganisation.ts
│ ├── useOrganisationPricingPlan.ts
│ ├── usePinnedGroups.ts
│ ├── usePlayerAuthActivities.ts
│ ├── usePlayerEvents.ts
│ ├── usePlayerHeadlines.ts
│ ├── usePlayerLeaderboardEntries.ts
│ ├── usePlayerSaves.ts
│ ├── usePlayerStats.ts
│ ├── usePlayers.ts
│ ├── usePricingPlanUsage.ts
│ ├── usePricingPlans.ts
│ ├── useStats.ts
│ ├── verify2FA.ts
│ └── viewRecoveryCodes.ts
├── assets
│ ├── talo-icon.svg
│ └── talo-service.svg
├── components
│ ├── Account2FA.tsx
│ ├── ActivityRenderer.tsx
│ ├── AlertBanner.tsx
│ ├── Button.tsx
│ ├── Checkbox.tsx
│ ├── CheckboxButton.tsx
│ ├── ConfirmEmailBanner.tsx
│ ├── DateInput.tsx
│ ├── DevDataStatus.tsx
│ ├── DropdownMenu.tsx
│ ├── ErrorMessage.tsx
│ ├── Footer.tsx
│ ├── GameSwitcher.tsx
│ ├── GlobalBanners.tsx
│ ├── HeadlineStat.tsx
│ ├── Identifier.tsx
│ ├── IntendedRouteHandler.tsx
│ ├── Link.tsx
│ ├── LinkButton.tsx
│ ├── Loading.tsx
│ ├── MobileMenu.tsx
│ ├── Modal.tsx
│ ├── NavBar.tsx
│ ├── Page.tsx
│ ├── Pagination.tsx
│ ├── PlayerAliases.tsx
│ ├── PlayerIdentifier.tsx
│ ├── PropsEditor.tsx
│ ├── RadioGroup.tsx
│ ├── RecoveryCodes.tsx
│ ├── SecondaryNav.tsx
│ ├── SecondaryTitle.tsx
│ ├── Select.tsx
│ ├── ServicesLink.tsx
│ ├── TaloInfoCard.tsx
│ ├── TextInput.tsx
│ ├── Tile.tsx
│ ├── TimePeriodPicker.tsx
│ ├── Title.tsx
│ ├── UsageWarningBanner.tsx
│ ├── __tests__
│ │ ├── Account2FA.test.tsx
│ │ ├── ConfirmEmailBanner.test.tsx
│ │ ├── DateInput.test.tsx
│ │ ├── DevDataStatus.test.tsx
│ │ ├── DropdownMenu.test.tsx
│ │ ├── ErrorMessage.test.tsx
│ │ ├── GameSwitcher.test.tsx
│ │ ├── GlobalBanners.test.tsx
│ │ ├── Link.test.tsx
│ │ ├── MobileMenu.test.tsx
│ │ ├── Modal.test.tsx
│ │ ├── NavBar.test.tsx
│ │ ├── Pagination.test.tsx
│ │ ├── PlayerAliases.test.tsx
│ │ ├── PlayerIdentifier.test.tsx
│ │ ├── RecoveryCodes.test.tsx
│ │ ├── SecondaryNav.test.tsx
│ │ ├── ServicesLink.test.tsx
│ │ ├── TimePeriodPicker.test.tsx
│ │ └── Title.test.tsx
│ ├── billing
│ │ ├── BillingPortalTile.tsx
│ │ ├── BillingUsageTile.tsx
│ │ ├── PaymentRequiredBanner.tsx
│ │ ├── PricingPlanTile.tsx
│ │ ├── RegisterPlanBanner.tsx
│ │ └── __tests__
│ │ │ ├── BillingPortalTile.test.tsx
│ │ │ ├── BillingUsageTile.test.tsx
│ │ │ ├── PaymentRequiredBanner.test.tsx
│ │ │ ├── PricingPlanTile.test.tsx
│ │ │ └── RegisterPlanBanner.test.tsx
│ ├── charts
│ │ ├── ChartTick.tsx
│ │ ├── ChartTooltip.tsx
│ │ └── __tests__
│ │ │ ├── ChartTick.test.tsx
│ │ │ └── ChartTooltip.test.tsx
│ ├── events
│ │ ├── EventsContext.tsx
│ │ ├── EventsDisplay.tsx
│ │ ├── EventsFilter.tsx
│ │ ├── EventsFiltersSection.tsx
│ │ └── useEventsTimePeriod.tsx
│ ├── saves
│ │ ├── SaveContentFitManager.tsx
│ │ └── SaveDataNode.tsx
│ ├── tables
│ │ ├── Table.tsx
│ │ ├── TableBody.tsx
│ │ ├── TableCell.tsx
│ │ ├── TableHeader.tsx
│ │ └── cells
│ │ │ └── DateCell.tsx
│ ├── toast
│ │ ├── ToastContext.ts
│ │ ├── ToastProvider.test.tsx
│ │ └── ToastProvider.tsx
│ └── toggles
│ │ ├── DevDataToggle.tsx
│ │ ├── Toggle.tsx
│ │ └── __tests__
│ │ ├── DevDataToggle.test.tsx
│ │ └── Toggle.test.tsx
├── constants
│ ├── metaProps.ts
│ ├── routes.ts
│ ├── secondaryNavRoutes.ts
│ └── userTypeMap.ts
├── entities
│ ├── apiKey.ts
│ ├── dataExport.ts
│ ├── event.ts
│ ├── game.ts
│ ├── gameActivity.ts
│ ├── gameChannels.ts
│ ├── gameFeedback.ts
│ ├── gameFeedbackCategory.ts
│ ├── gameSave.ts
│ ├── gameStat.ts
│ ├── headline.ts
│ ├── integration.ts
│ ├── invite.ts
│ ├── invoice.ts
│ ├── leaderboard.ts
│ ├── leaderboardEntry.ts
│ ├── organisation.ts
│ ├── player.ts
│ ├── playerAlias.ts
│ ├── playerAuthActivity.ts
│ ├── playerGameStat.ts
│ ├── playerGroup.ts
│ ├── playerHeadline.ts
│ ├── playerPresence.ts
│ ├── pricingPlan.ts
│ ├── prop.ts
│ └── user.ts
├── index.tsx
├── modals
│ ├── ChannelDetails.tsx
│ ├── ConfirmPlanChange.tsx
│ ├── FeedbackCategoryDetails.tsx
│ ├── IntegrationDetails.tsx
│ ├── LeaderboardDetails.tsx
│ ├── NewGame.tsx
│ ├── NewInvite.tsx
│ ├── Scopes.tsx
│ ├── StatDetails.tsx
│ ├── UpdateEntryScore.tsx
│ ├── UpdateStatValue.tsx
│ ├── __tests__
│ │ ├── ConfirmPlanChange.test.tsx
│ │ ├── FeedbackCategoryDetails.test.tsx
│ │ ├── IntegrationDetails.test.tsx
│ │ ├── LeaderboardDetails.test.tsx
│ │ ├── NewGame.test.tsx
│ │ ├── Scopes.test.tsx
│ │ └── StatDetails.test.tsx
│ └── groups
│ │ ├── GroupDetails.tsx
│ │ ├── GroupRules.tsx
│ │ └── __tests__
│ │ ├── GroupDetails.test.tsx
│ │ ├── GroupRules.test.tsx
│ │ └── unpackRules.test.tsx
├── pages
│ ├── APIKeys.tsx
│ ├── AcceptInvite.tsx
│ ├── Account.tsx
│ ├── Activity.tsx
│ ├── Billing.tsx
│ ├── Channels.tsx
│ ├── ConfirmPassword.tsx
│ ├── Dashboard.tsx
│ ├── DataExports.tsx
│ ├── Demo.tsx
│ ├── EventBreakdown.tsx
│ ├── EventsOverview.tsx
│ ├── Feedback.tsx
│ ├── FeedbackCategories.tsx
│ ├── ForgotPassword.tsx
│ ├── GameProps.tsx
│ ├── GameSettings.tsx
│ ├── Groups.tsx
│ ├── Integrations.tsx
│ ├── LeaderboardEntries.tsx
│ ├── Leaderboards.tsx
│ ├── Login.tsx
│ ├── NotFound.tsx
│ ├── Organisation.tsx
│ ├── PlayerEvents.tsx
│ ├── PlayerLeaderboardEntries.tsx
│ ├── PlayerProfile.tsx
│ ├── PlayerProps.tsx
│ ├── PlayerSaveContent.tsx
│ ├── PlayerSaves.tsx
│ ├── PlayerStats.tsx
│ ├── Players.tsx
│ ├── RecoverAccount.tsx
│ ├── Register.tsx
│ ├── ResetPassword.tsx
│ ├── Stats.tsx
│ ├── Verify2FA.tsx
│ └── __tests__
│ │ ├── Activity.test.tsx
│ │ ├── Billing.test.tsx
│ │ ├── Dashboard.test.tsx
│ │ ├── ForgotPassword.test.tsx
│ │ ├── Integrations.test.tsx
│ │ ├── PlayerProps.test.tsx
│ │ ├── Register.test.tsx
│ │ └── ResetPassword.test.tsx
├── services
│ ├── AuthService.ts
│ └── __tests__
│ │ └── AuthService.test.ts
├── state
│ ├── RecoilObserver.tsx
│ ├── activeGameState.ts
│ ├── devDataState.ts
│ ├── gamesState.ts
│ ├── justConfirmedEmailState.ts
│ ├── organisationState.ts
│ ├── saveDataNodeSizesState.ts
│ └── userState.ts
├── styles
│ ├── index.css
│ └── theme.ts
├── utils
│ ├── KitchenSink.tsx
│ ├── __tests__
│ │ ├── buildError.test.ts
│ │ ├── canPerformAction.test.ts
│ │ ├── usePlayer.test.tsx
│ │ ├── useSortedItems.test.tsx
│ │ └── useTimePeriod.test.tsx
│ ├── buildError.ts
│ ├── canPerformAction.ts
│ ├── canViewPage.ts
│ ├── getEventColour.ts
│ ├── group-rules
│ │ ├── __tests__
│ │ │ ├── isGroupRuleValid.test.ts
│ │ │ └── prepareRule.test.ts
│ │ ├── isGroupRuleValid.ts
│ │ ├── prepareRule.ts
│ │ └── unpackRules.ts
│ ├── useDaySections.ts
│ ├── useIntendedRoute.ts
│ ├── useLocalStorage.ts
│ ├── useNodeGraph.ts
│ ├── usePlayer.ts
│ ├── useSearch.ts
│ ├── useSortedItems.ts
│ ├── useTimePeriod.ts
│ └── validation
│ │ └── nullableNumber.ts
└── vite-env.d.ts
├── tailwind.config.js
├── tsconfig.build.json
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts
/.dockerignore:
--------------------------------------------------------------------------------
1 | .git
2 | .github
3 | node_modules
4 | .env.development
5 | .eslintrc.json
6 | build
7 |
--------------------------------------------------------------------------------
/.env.development:
--------------------------------------------------------------------------------
1 | VITE_API_URL=http://localhost:3000
2 |
3 | VITE_SENTRY_DSN=
4 | VITE_SENTRY_ENV=dev
5 |
--------------------------------------------------------------------------------
/.env.e2e:
--------------------------------------------------------------------------------
1 | VITE_API_URL=http://talo.api
2 |
3 | VITE_SENTRY_DSN=
4 | VITE_SENTRY_ENV=dev
5 |
--------------------------------------------------------------------------------
/.env.production:
--------------------------------------------------------------------------------
1 | VITE_API_URL=${API_URL}
2 |
3 | VITE_SENTRY_DSN=${SENTRY_DSN}
4 | VITE_SENTRY_ENV=prod
5 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: TaloDev
2 |
--------------------------------------------------------------------------------
/.github/release.yml:
--------------------------------------------------------------------------------
1 | changelog:
2 | exclude:
3 | labels:
4 | - release
5 | categories:
6 | - title: Breaking changes
7 | labels:
8 | - breaking
9 | - title: Features
10 | labels:
11 | - enhancement
12 | - title: Fixes
13 | labels:
14 | - fix
15 | - title: Other
16 | labels:
17 | - "*"
18 |
--------------------------------------------------------------------------------
/.github/workflows/create-release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | workflow_dispatch:
5 |
6 | jobs:
7 | release:
8 | runs-on: ubuntu-latest
9 | steps:
10 | - uses: actions/checkout@v4
11 | with:
12 | fetch-depth: 0
13 |
14 | - uses: actions/setup-node@v4
15 | with:
16 | node-version: 20
17 |
18 | - uses: actions/cache@v4
19 | with:
20 | path: '**/node_modules'
21 | key: ${{ runner.os }}-modules-${{ hashFiles('**/package-lock.json') }}
22 |
23 | - name: Install deps
24 | run: npm ci --prefer-offline
25 |
26 | - name: Build
27 | env:
28 | ENABLE_SOURCEMAPS: true
29 | run: npm run build
30 |
31 | - name: Create GitHub release
32 | uses: softprops/action-gh-release@v2
33 | if: "!contains(github.event.head_commit.message, '--no-release')"
34 | with:
35 | generate_release_notes: true
36 |
37 | - name: Create Sentry release
38 | uses: getsentry/action-release@v1
39 | env:
40 | SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
41 | SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
42 | SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
43 | with:
44 | environment: prod
45 | sourcemaps: './build'
46 |
--------------------------------------------------------------------------------
/.github/workflows/docker.yml:
--------------------------------------------------------------------------------
1 | name: Docker build
2 |
3 | on:
4 | workflow_dispatch:
5 |
6 | jobs:
7 | build:
8 | runs-on: ubuntu-latest
9 | steps:
10 | - name: Set up QEMU
11 | uses: docker/setup-qemu-action@v3
12 |
13 | - name: Set up Docker Buildx
14 | uses: docker/setup-buildx-action@v3
15 |
16 | - name: Login to DockerHub
17 | uses: docker/login-action@v3
18 | with:
19 | registry: ghcr.io
20 | username: ${{ github.actor }}
21 | password: ${{ secrets.GITHUB_TOKEN }}
22 |
23 | - name: Build and push
24 | id: docker_build
25 | uses: docker/build-push-action@v5
26 | with:
27 | platforms: linux/amd64,linux/arm64
28 | push: true
29 | tags: |
30 | ghcr.io/talodev/frontend:latest
31 | ghcr.io/talodev/frontend:${{ github.ref_name }}
32 |
33 | - name: Image digest
34 | run: echo ${{ steps.docker_build.outputs.digest }}
35 |
--------------------------------------------------------------------------------
/.github/workflows/tag.yml:
--------------------------------------------------------------------------------
1 | name: Tag
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 |
7 | jobs:
8 | tag:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v4
12 |
13 | - name: Check version change
14 | id: check
15 | uses: EndBug/version-check@v2
16 |
17 | - name: Create tag
18 | if: steps.check.outputs.changed == 'true'
19 | uses: tvdias/github-tagger@v0.0.2
20 | with:
21 | repo-token: ${{ secrets.GITHUB_TOKEN }}
22 | tag: ${{ steps.check.outputs.version }}
23 |
24 | - name: Trigger Docker workflow
25 | if: steps.check.outputs.changed == 'true'
26 | uses: actions/github-script@v7
27 | with:
28 | script: |
29 | github.rest.actions.createWorkflowDispatch({
30 | owner: context.repo.owner,
31 | repo: context.repo.repo,
32 | workflow_id: 'docker.yml',
33 | ref: '${{ steps.check.outputs.version }}'
34 | })
35 |
36 | - name: Trigger Release workflow
37 | if: steps.check.outputs.changed == 'true'
38 | uses: actions/github-script@v7
39 | with:
40 | script: |
41 | github.rest.actions.createWorkflowDispatch({
42 | owner: context.repo.owner,
43 | repo: context.repo.repo,
44 | workflow_id: 'create-release.yml',
45 | ref: '${{ steps.check.outputs.version }}'
46 | })
47 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | dist
2 | node_modules
3 | *.log
4 | .DS_Store
5 | .eslintcache
6 | coverage
7 |
--------------------------------------------------------------------------------
/.husky/.gitignore:
--------------------------------------------------------------------------------
1 | _
2 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | npx lint-staged
2 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | Want to add a new system? Are the docs not clear enough? We're always accepting contributions so please share any new features, improvements or bug fixes with us.
4 |
5 | ## Installing, building & running
6 |
7 | Run `npm install` to install the dependencies.
8 |
9 | ### npm run dev
10 |
11 | Runs the app in the development mode.
12 | Open http://localhost:8080 to view it in the browser.
13 |
14 | ### npm run build
15 |
16 | Builds a static copy of the site to the `dist/` folder.
17 |
18 | ## Docker?
19 |
20 | We use Docker to build a production image of the dashboard (which is a simple NGINX server that hosts the static build files). It's not needed for development.
21 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:20-alpine AS build
2 | WORKDIR /usr/frontend
3 | COPY . .
4 |
5 | # prepend \ to variables that need to be substituted
6 | # this prevents them from being substituted with empty values
7 | RUN sed -i 's/\${/\\${/g' .env.production
8 |
9 | RUN npm install
10 | RUN npm run build
11 |
12 | FROM caddy:2-alpine
13 | COPY --from=build /usr/frontend/dist /srv
14 | COPY /config/Caddyfile /etc/caddy/Caddyfile
15 | COPY --from=build /usr/frontend/config/entrypoint.sh /usr/local/bin/
16 |
17 | RUN apk add --no-cache nodejs npm
18 | RUN npm i envsub -g
19 | RUN chmod +x /usr/local/bin/entrypoint.sh
20 |
21 | ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
22 |
23 | EXPOSE 80
24 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Talo Platform Ltd
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/config/Caddyfile:
--------------------------------------------------------------------------------
1 | :80 {
2 | root * /srv
3 | encode gzip
4 |
5 | try_files {path} /index.html
6 | file_server
7 |
8 | handle_errors {
9 | rewrite * /index.html
10 | file_server
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/config/entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | for filename in /srv/assets/*.js; do
3 | envsub --protect $filename $filename
4 | done
5 |
6 | caddy run --config /etc/caddy/Caddyfile
7 |
--------------------------------------------------------------------------------
/cypress.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'cypress'
2 |
3 | export default defineConfig({
4 | e2e: {
5 | baseUrl: 'http://localhost:8080',
6 | setupNodeEvents(on, config) {
7 | // implement node event listeners here
8 | }
9 | }
10 | })
11 |
--------------------------------------------------------------------------------
/cypress/e2e/pages/AcceptInvite.spec.cy.js:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | describe('AcceptInvite', () => {
4 | it('should let users accept an invite', () => {
5 | cy.intercept('GET', /http:\/\/talo\.api\/public\/invites\/(.*)/, {
6 | statusCode: 200,
7 | body: {
8 | invite: {
9 | id: 1,
10 | email: 'dev@trytalo.com',
11 | organisation: {
12 | name: 'Sleepy Studios'
13 | },
14 | invitedBy: 'Owner',
15 | createdAt: '2023-01-01 00:00:00'
16 | }
17 | }
18 | })
19 |
20 | cy.intercept('POST', 'http://talo.api/public/users/register', {
21 | statusCode: 200,
22 | fixture: 'responses/auth/dev'
23 | })
24 |
25 | cy.visitAsGuest('/accept/TRPEPI0BXC')
26 | cy.findByText('Sleepy Studios').should('exist')
27 |
28 | cy.findByText('Let\'s get started').should('exist')
29 | cy.findByLabelText('Username').type('sleepy')
30 | cy.findByLabelText('Password').type('password')
31 |
32 | cy.findByText('Sign up').should('be.disabled')
33 | cy.findByRole('checkbox').click()
34 | cy.findByText('Sign up').should('be.enabled')
35 | cy.findByText('Sign up').click()
36 |
37 | cy.findByText('Superstatic dashboard').should('exist')
38 | })
39 |
40 | it('should handle missing invites', () => {
41 | cy.intercept('GET', /http:\/\/talo\.api\/public\/invites\/(.*)/, {
42 | statusCode: 404,
43 | body: {
44 | message: 'Invite not found'
45 | }
46 | })
47 |
48 | cy.visitAsGuest('/accept/TRPEPI0BXC')
49 | cy.findByText('Invite not found').should('exist')
50 | })
51 | })
52 |
--------------------------------------------------------------------------------
/cypress/e2e/pages/Demo.spec.cy.js:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | describe('Demo', () => {
4 | it('should let users enter the demo dashboard', () => {
5 | cy.intercept('POST', 'http://talo.api/public/demo', {
6 | statusCode: 200,
7 | fixture: 'responses/auth/dev'
8 | })
9 |
10 | cy.visitAsGuest('/demo')
11 | cy.findByText('Launch demo').click()
12 |
13 | cy.findByText('Superstatic dashboard').should('exist')
14 | })
15 |
16 | it('should handle errors', () => {
17 | cy.intercept('POST', 'http://talo.api/public/demo', {
18 | statusCode: 500,
19 | body: {
20 | message: 'Something went wrong'
21 | }
22 | })
23 |
24 | cy.visitAsGuest('/demo')
25 | cy.findByText('Launch demo').click()
26 |
27 | cy.findByText('Something went wrong').should('exist')
28 | })
29 | })
30 |
--------------------------------------------------------------------------------
/cypress/e2e/pages/ForgotPassword.spec.cy.js:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | describe('ForgotPassword', () => {
4 | it('should let users request a reset link', () => {
5 | cy.intercept('POST', 'http://talo.api/public/users/forgot_password', {
6 | statusCode: 204
7 | })
8 |
9 | cy.visitAsGuest()
10 | cy.findByText('Forgot your password?').click()
11 | cy.findByLabelText('Email').type('admin@trytalo.com')
12 | cy.findByText('Confirm').click()
13 |
14 | cy.findByText('If an account exists for this email, you\'ll receive an email with instructions on how to reset your password').should('exist')
15 | })
16 | })
17 |
--------------------------------------------------------------------------------
/cypress/e2e/pages/Register.spec.cy.js:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | describe('Register', () => {
4 | it('should let users register and create a new game', () => {
5 | cy.intercept('POST', 'http://talo.api/public/users/register', {
6 | statusCode: 200,
7 | fixture: 'responses/auth/register'
8 | })
9 |
10 | cy.visitAsGuest()
11 | cy.findByText('Register here').click()
12 |
13 | cy.findByText('Let\'s get started').should('exist')
14 | cy.findByLabelText('Team or studio name').type('Sleepy Studios')
15 | cy.findByLabelText('Username').type('sleepy')
16 | cy.findByLabelText('Email').type('admin@trytalo.com')
17 | cy.findByLabelText('Password').type('password')
18 |
19 | cy.findByText('Sign up').should('be.disabled')
20 | cy.findByRole('checkbox').click()
21 | cy.findByText('Sign up').should('be.enabled')
22 | cy.findByText('Sign up').click()
23 |
24 | cy.findByText('New game').should('exist')
25 | cy.findByText('New game').click()
26 |
27 | cy.findByText('Create new game').should('exist')
28 | })
29 | })
30 |
--------------------------------------------------------------------------------
/cypress/e2e/pages/ResetPassword.spec.cy.js:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | describe('ResetPassword', () => {
4 | it('should let users reset their password', () => {
5 | cy.intercept('POST', 'http://talo.api/public/users/reset_password', {
6 | statusCode: 204
7 | })
8 |
9 | cy.visitAsGuest('/reset-password?token=abc123')
10 | cy.findByLabelText('New password').type('p@ssw0rd')
11 | cy.findByLabelText('Confirm password').type('p@ssw0rd1')
12 |
13 | cy.findByText('Confirm').should('be.disabled')
14 | cy.findByLabelText('New password').type('1')
15 | cy.findByText('Confirm').should('be.enabled')
16 | cy.findByText('Confirm').click()
17 |
18 | cy.findByText('Success! Your password has been reset').should('exist')
19 | cy.findByText('Go to Login').click()
20 | cy.findByText('Talo Game Services').should('exist')
21 | })
22 |
23 | it('should let users login with valid credentials', () => {
24 | cy.intercept('POST', 'http://talo.api/public/users/reset_password', {
25 | statusCode: 401,
26 | body: {
27 | message: 'Request expired',
28 | expired: true
29 | }
30 | })
31 |
32 | cy.visitAsGuest('/reset-password?token=abc123')
33 | cy.findByLabelText('New password').type('p@ssw0rd1')
34 | cy.findByLabelText('Confirm password').type('p@ssw0rd1')
35 | cy.findByText('Confirm').click()
36 |
37 | cy.findByText('please request a new reset link').should('exist')
38 | })
39 | })
40 |
--------------------------------------------------------------------------------
/cypress/e2e/pages/Verify2FA.spec.cy.js:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | describe('Verify2FA', () => {
4 | it('should let 2fa users log in', () => {
5 | cy.intercept('POST', 'http://talo.api/public/users/login', {
6 | statusCode: 200,
7 | body: {
8 | twoFactorAuthRequired: true,
9 | userId: 1
10 | }
11 | })
12 |
13 | cy.intercept('POST', 'http://talo.api/public/users/2fa', { fixture: 'responses/auth/dev' })
14 |
15 | cy.visitAsGuest()
16 | cy.findByLabelText('Email').type('admin@trytalo.com')
17 | cy.findByLabelText('Password').type('password')
18 | cy.findByText('Login').click()
19 |
20 | cy.findByText('Two factor authentication').should('exist')
21 | cy.findByLabelText('Code').type('123456')
22 | cy.findByText('Confirm').click()
23 |
24 | cy.findByText('Superstatic dashboard').should('exist')
25 | })
26 |
27 | it('should let 2fa users log in', () => {
28 | cy.intercept('POST', 'http://talo.api/public/users/login', {
29 | statusCode: 200,
30 | body: {
31 | twoFactorAuthRequired: true,
32 | userId: 1
33 | }
34 | })
35 |
36 | cy.intercept('POST', 'http://talo.api/public/users/2fa', {
37 | statusCode: 403,
38 | body: {
39 | message: 'Invalid code'
40 | }
41 | })
42 |
43 | cy.visitAsGuest()
44 | cy.findByLabelText('Email').type('admin@trytalo.com')
45 | cy.findByLabelText('Password').type('password')
46 | cy.findByText('Login').click()
47 |
48 | cy.findByText('Two factor authentication').should('exist')
49 | cy.findByLabelText('Code').type('123456')
50 | cy.findByText('Confirm').click()
51 |
52 | cy.findByText('Invalid code').should('exist')
53 | })
54 | })
55 |
--------------------------------------------------------------------------------
/cypress/fixtures/responses/auth/dev.json:
--------------------------------------------------------------------------------
1 | {
2 | "accessToken": "ey...",
3 | "user": {
4 | "id": 1,
5 | "type": 2,
6 | "username": "Dev",
7 | "email": "dev@trytalo.com",
8 | "organisation": {
9 | "games": [
10 | {
11 | "id": 1,
12 | "name": "Superstatic",
13 | "props": [],
14 | "playerCount": 0,
15 | "createdAt": "2023-01-01 00:00:00"
16 | }
17 | ]
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/cypress/fixtures/responses/auth/owner.json:
--------------------------------------------------------------------------------
1 | {
2 | "accessToken": "ey...",
3 | "user": {
4 | "id": 1,
5 | "type": 0,
6 | "username": "Owner",
7 | "organisation": {
8 | "games": [
9 | {
10 | "id": 1,
11 | "name": "Superstatic",
12 | "props": [],
13 | "playerCount": 0,
14 | "createdAt": "2023-01-01 00:00:00"
15 | }
16 | ],
17 | "pricingPlan": {
18 | "status": "active"
19 | }
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/cypress/fixtures/responses/auth/register.json:
--------------------------------------------------------------------------------
1 | {
2 | "accessToken": "ey...",
3 | "user": {
4 | "id": 1,
5 | "type": 2,
6 | "username": "sleepy",
7 | "organisation": {
8 | "games": []
9 | }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/cypress/fixtures/responses/billing/free-plan.json:
--------------------------------------------------------------------------------
1 | {
2 | "pricingPlan": {
3 | "pricingPlan": {
4 | "id": 1,
5 | "hidden": false,
6 | "default": true,
7 | "playerLimit": 10000
8 | },
9 | "status": "active",
10 | "endDate": null,
11 | "canViewBillingPortal": true
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/cypress/fixtures/responses/billing/invoice.json:
--------------------------------------------------------------------------------
1 | {
2 | "invoice": {
3 | "lines": [
4 | {
5 | "id": 1,
6 | "period": {
7 | "start": 1655024400,
8 | "end": 1657616400
9 | },
10 | "description": "Team plan usage",
11 | "amount": 5000
12 | },
13 | {
14 | "id": 2,
15 | "period": {
16 | "start": 1655024400,
17 | "end": 1657666799
18 | },
19 | "description": "Team plan proration",
20 | "amount": -300
21 | },
22 | {
23 | "id": 3,
24 | "period": {
25 | "start": 1657670400,
26 | "end": 1660345199
27 | },
28 | "description": "Studio plan usage",
29 | "amount": 8000
30 | }
31 | ],
32 | "total": 13300,
33 | "prorationDate": 1655024400,
34 | "collectionDate": 1660345199
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/cypress/fixtures/responses/billing/usage.json:
--------------------------------------------------------------------------------
1 | {
2 | "usage": {
3 | "limit": 2,
4 | "used": 0
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/cypress/fixtures/responses/game-stats/global.json:
--------------------------------------------------------------------------------
1 | {
2 | "stats": [
3 | {
4 | "id": 1,
5 | "internalName": "damage-done",
6 | "name": "Damage done",
7 | "global": true,
8 | "globalValue": 56,
9 | "defaultValue": 0,
10 | "maxChange": 999,
11 | "minValue": 0,
12 | "maxValue": null,
13 | "minTimeBetweenUpdates": 0,
14 | "createdAt": "2023-01-01 00:00:00",
15 | "updatedAt": "2023-01-01 00:00:00"
16 | },
17 | {
18 | "id": 2,
19 | "internalName": "treasures-found",
20 | "name": "Treasures found",
21 | "global": true,
22 | "globalValue": 14,
23 | "defaultValue": 0,
24 | "maxChange": 1,
25 | "minValue": 0,
26 | "maxValue": 30,
27 | "minTimeBetweenUpdates": 0,
28 | "createdAt": "2023-01-01 00:00:00",
29 | "updatedAt": "2023-01-01 00:00:00"
30 | }
31 | ]
32 | }
33 |
--------------------------------------------------------------------------------
/cypress/support/commands.js:
--------------------------------------------------------------------------------
1 | import '@testing-library/cypress/add-commands'
2 |
3 | Cypress.Commands.add('stubDashboardCalls', () => {
4 | cy.intercept('GET', /http:\/\/talo\.api\/games\/1\/headlines\/average_session_duration/, {
5 | statusCode: 200,
6 | body: {
7 | hours: Cypress._.random(0, 2),
8 | minutes: Cypress._.random(0, 59),
9 | seconds: Cypress._.random(0, 59)
10 | }
11 | })
12 |
13 | cy.intercept('GET', /http:\/\/talo\.api\/games\/1\/headlines\/(.*)/, {
14 | statusCode: 200,
15 | body: {
16 | count: Cypress._.random(0, 100)
17 | }
18 | })
19 |
20 | cy.intercept('GET', 'http://talo.api/games/1/game-stats', {
21 | statusCode: 200,
22 | fixture: 'responses/game-stats/global'
23 | })
24 | })
25 |
26 | Cypress.Commands.add('visitAsGuest', (url = '/') => {
27 | cy.intercept('GET', 'http://talo.api/public/users/refresh', {
28 | statusCode: 401
29 | })
30 |
31 | cy.stubDashboardCalls()
32 |
33 | cy.visit(url)
34 | })
35 |
36 | Cypress.Commands.add('login', (userType = 'dev', url = '/') => {
37 | cy.intercept('GET', 'http://talo.api/public/users/refresh', {
38 | statusCode: 200,
39 | fixture: `responses/auth/${userType}`
40 | })
41 |
42 | cy.stubDashboardCalls()
43 |
44 | cy.visit(url)
45 | })
46 |
--------------------------------------------------------------------------------
/cypress/support/e2e.js:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example support/e2e.js is processed and
3 | // loaded automatically before your test files.
4 | //
5 | // This is a great place to put global configuration and
6 | // behavior that modifies Cypress.
7 | //
8 | // You can change the location of this file or turn off
9 | // automatically serving support files with the
10 | // 'supportFile' configuration option.
11 | //
12 | // You can read more here:
13 | // https://on.cypress.io/configuration
14 | // ***********************************************************
15 |
16 | // Import commands.js using ES2015 syntax:
17 | import './commands'
18 |
19 | // Alternatively you can use CommonJS syntax:
20 | // require('./commands')
21 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Talo Dashboard
9 |
10 |
11 |
12 | You need to enable JavaScript to run this app.
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | 'tailwindcss/nesting': {},
4 | tailwindcss: {},
5 | autoprefixer: {}
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TaloDev/frontend/eeef2e4c98ee84b8c34c23f2e7ae303ed5e3365a/public/favicon.ico
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": [
4 | "config:base"
5 | ],
6 | "packageRules": [
7 | {
8 | "matchDepTypes": ["devDependencies"],
9 | "matchUpdateTypes": ["minor", "patch"],
10 | "automerge": true
11 | },
12 | {
13 | "matchDepTypes": ["dependencies"],
14 | "matchUpdateTypes": ["patch"],
15 | "enabled": false
16 | }
17 | ],
18 | "platformAutomerge": true,
19 | "schedule": ["before 3am on the first day of the month"]
20 | }
21 |
--------------------------------------------------------------------------------
/setup-tests.js:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom'
2 |
3 | globalThis.IS_REACT_ACT_ENVIRONMENT = true
4 |
5 | // eslint-disable-next-line no-undef
6 | process.env.VITE_API_URL = 'http://talo.api'
7 |
--------------------------------------------------------------------------------
/src/__mocks__/gameFeedbackCategoryMock.ts:
--------------------------------------------------------------------------------
1 | import { GameFeedbackCategory } from '../entities/gameFeedbackCategory'
2 |
3 | export default function gameFeedbackCategoryMock(extra: Partial = {}): GameFeedbackCategory {
4 | return {
5 | id: 1,
6 | internalName: 'bugs',
7 | name: 'Bugs',
8 | description: 'Bug reports',
9 | anonymised: false,
10 | createdAt: new Date().toISOString(),
11 | updatedAt: new Date().toISOString(),
12 | ...extra
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/__mocks__/gameMock.ts:
--------------------------------------------------------------------------------
1 | import { Game } from '../entities/game'
2 |
3 | export default function gameMock(activeGameValue: { id: string, name: string }, extra: Partial = {}): Game {
4 | return {
5 | id: Number(activeGameValue.id),
6 | name: activeGameValue.name,
7 | props: [],
8 | playerCount: 0,
9 | createdAt: new Date().toISOString(),
10 | ...extra
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/__mocks__/gameStatMock.ts:
--------------------------------------------------------------------------------
1 | import { GameStat } from '../entities/gameStat'
2 |
3 | export default function gameStatMock(extra: Partial = {}): GameStat {
4 | return {
5 | id: 1,
6 | internalName: 'hearts-collected',
7 | name: 'Hearts collected',
8 | global: false,
9 | globalValue: 5,
10 | minValue: null,
11 | defaultValue: 5,
12 | maxValue: null,
13 | maxChange: null,
14 | minTimeBetweenUpdates: 0,
15 | createdAt: new Date().toISOString(),
16 | updatedAt: new Date().toISOString(),
17 | ...extra
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/__mocks__/leaderboardMock.ts:
--------------------------------------------------------------------------------
1 | import { Leaderboard, LeaderboardSortMode, LeaderboardRefreshInterval } from '../entities/leaderboard'
2 |
3 | export default function leaderboardMock(extra: Partial = {}): Leaderboard {
4 | return {
5 | id: 1,
6 | internalName: 'score',
7 | name: 'Score',
8 | sortMode: LeaderboardSortMode.ASC,
9 | unique: true,
10 | createdAt: new Date().toISOString(),
11 | updatedAt: new Date().toISOString(),
12 | refreshInterval: LeaderboardRefreshInterval.NEVER,
13 | ...extra
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/__mocks__/organisationMock.ts:
--------------------------------------------------------------------------------
1 | import { Organisation, statusSchema } from '../entities/organisation'
2 |
3 | export default function organisationMock(extra: Partial = {}): Organisation {
4 | return {
5 | id: 1,
6 | name: 'Sleepy Studios',
7 | games: [],
8 | pricingPlan: {
9 | status: statusSchema.enum.active
10 | },
11 | ...extra
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/__mocks__/playerAliasMock.ts:
--------------------------------------------------------------------------------
1 | import { PlayerAlias, PlayerAliasService } from '../entities/playerAlias'
2 | import playerMock from './playerMock'
3 |
4 | export default function playerAliasMock(extra: Partial = {}): PlayerAlias {
5 | return {
6 | id: 1,
7 | service: PlayerAliasService.STEAM,
8 | identifier: 'yxre12',
9 | player: playerMock(),
10 | lastSeenAt: new Date().toISOString(),
11 | createdAt: new Date().toISOString(),
12 | updatedAt: new Date().toISOString(),
13 | ...extra
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/__mocks__/playerMock.ts:
--------------------------------------------------------------------------------
1 | import { Player } from '../entities/player'
2 |
3 | export default function playerMock(extra: Partial = {}): Player {
4 | return {
5 | id: '031fcd24-8ac4-4a64-ab5e-e86d05e7fe89',
6 | props: [],
7 | devBuild: true,
8 | createdAt: new Date().toISOString(),
9 | lastSeenAt: new Date().toISOString(),
10 | aliases: [],
11 | groups: [],
12 | presence: null,
13 | ...extra
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/__mocks__/pricingPlanMock.ts:
--------------------------------------------------------------------------------
1 | import { PricingPlanProduct } from '../entities/pricingPlan'
2 |
3 | export default function pricingPlanMock(extra: Partial = {}): PricingPlanProduct {
4 | return {
5 | id: 1,
6 | name: 'Team plan',
7 | prices: [
8 | { amount: 5999, currency: 'usd', interval: 'year', current: false },
9 | { amount: 499, currency: 'usd', interval: 'month', current: false }
10 | ],
11 | stripeId: 'plan_1',
12 | hidden: false,
13 | default: false,
14 | playerLimit: 10000,
15 | ...extra
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/__mocks__/userMock.ts:
--------------------------------------------------------------------------------
1 | import { User, UserType } from '../entities/user'
2 | import organisationMock from './organisationMock'
3 |
4 | export default function userMock(extra: Partial = {}): User {
5 | return {
6 | id: 1,
7 | email: 'dev@trytalo.com',
8 | username: 'dev',
9 | lastSeenAt: new Date().toISOString(),
10 | emailConfirmed: false,
11 | organisation: organisationMock(),
12 | type: UserType.DEV,
13 | has2fa: false,
14 | createdAt: new Date().toISOString(),
15 | ...extra
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/api/api.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | import AuthService from '../services/AuthService'
3 | import refreshAccess from './refreshAccess'
4 |
5 | export const apiConfig = {
6 | withCredentials: true,
7 | baseURL: import.meta.env.VITE_API_URL
8 | }
9 |
10 | const instance = axios.create(apiConfig)
11 |
12 | instance.interceptors.request.use((config) => {
13 | if (AuthService.getToken()) {
14 | config.headers['Authorization'] = `Bearer ${AuthService.getToken()}`
15 | }
16 |
17 | const includeDevData = (window.localStorage.getItem('includeDevDataOptimistic') ??
18 | window.localStorage.getItem('includeDevData')) === 'true'
19 |
20 | config.headers['X-Talo-Include-Dev-Data'] = includeDevData ? '1' : '0'
21 |
22 | return config
23 | }, (error) => Promise.reject(error)
24 | )
25 |
26 | instance.interceptors.response.use((response) => {
27 | return response
28 | }, async (error) => {
29 | const request = error.config
30 |
31 | if (!request.url.startsWith('/public')) {
32 | if (error.response?.status === 401 && !request._retry) {
33 | request._retry = true
34 |
35 | const { accessToken } = await refreshAccess()
36 | const newToken = accessToken
37 |
38 | AuthService.setToken(newToken)
39 | axios.defaults.headers.common['Authorization'] = `Bearer ${newToken}`
40 |
41 | return instance(request)
42 | } else if (error.response?.status === 401 && request._retry) {
43 | AuthService.reload()
44 | }
45 | }
46 |
47 | return Promise.reject(error)
48 | })
49 |
50 | export default instance
51 |
--------------------------------------------------------------------------------
/src/api/changePassword.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod'
2 | import api from './api'
3 | import makeValidatedRequest from './makeValidatedRequest'
4 |
5 | const changePassword = makeValidatedRequest(
6 | (currentPassword: string, newPassword: string) => api.post('/users/change_password', { currentPassword, newPassword }),
7 | z.object({
8 | accessToken: z.string()
9 | })
10 | )
11 |
12 | export default changePassword
13 |
--------------------------------------------------------------------------------
/src/api/confirmEmail.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod'
2 | import api from './api'
3 | import makeValidatedRequest from './makeValidatedRequest'
4 | import { userSchema } from '../entities/user'
5 |
6 | const confirmEmail = makeValidatedRequest(
7 | (code: string) => api.post('/users/confirm_email', { code }),
8 | z.object({
9 | user: userSchema
10 | })
11 | )
12 |
13 | export default confirmEmail
14 |
--------------------------------------------------------------------------------
/src/api/confirmEnable2FA.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod'
2 | import api from './api'
3 | import makeValidatedRequest from './makeValidatedRequest'
4 | import { userSchema } from '../entities/user'
5 |
6 | const confirmEnable2FA = makeValidatedRequest(
7 | (code: string) => api.post('/users/2fa/enable', { code }),
8 | z.object({
9 | user: userSchema,
10 | recoveryCodes: z.array(z.string())
11 | })
12 | )
13 |
14 | export default confirmEnable2FA
15 |
16 |
--------------------------------------------------------------------------------
/src/api/confirmPlan.ts:
--------------------------------------------------------------------------------
1 | import api from './api'
2 |
3 | const confirmPlan = (prorationDate: number, pricingPlanId: number, pricingInterval: string) =>
4 | api.post('/billing/confirm-plan', { prorationDate, pricingPlanId, pricingInterval })
5 |
6 | export default confirmPlan
7 |
--------------------------------------------------------------------------------
/src/api/createAPIKey.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod'
2 | import api from './api'
3 | import makeValidatedRequest from './makeValidatedRequest'
4 | import { apiKeySchema } from '../entities/apiKey'
5 |
6 | const createAPIKey = makeValidatedRequest(
7 | (gameId: number, scopes: string[]) => api.post(`/games/${gameId}/api-keys`, { scopes }),
8 | z.object({
9 | token: z.string(),
10 | apiKey: apiKeySchema
11 | })
12 | )
13 |
14 | export default createAPIKey
15 |
16 |
--------------------------------------------------------------------------------
/src/api/createChannel.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod'
2 | import { GameChannel, gameChannelSchema } from '../entities/gameChannels'
3 | import api from './api'
4 | import makeValidatedRequest from './makeValidatedRequest'
5 |
6 | type Data = Pick & { ownerAliasId: number | null }
7 |
8 | const createChannel = makeValidatedRequest(
9 | (gameId: number, data: Data) => api.post(`/games/${gameId}/game-channels`, data),
10 | z.object({
11 | channel: gameChannelSchema
12 | })
13 | )
14 |
15 | export default createChannel
16 |
--------------------------------------------------------------------------------
/src/api/createCheckoutSession.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod'
2 | import api from './api'
3 | import makeValidatedRequest from './makeValidatedRequest'
4 | import { invoiceSchema } from '../entities/invoice'
5 |
6 | const createCheckoutSession = makeValidatedRequest(
7 | (pricingPlanId, pricingInterval) => api.post('/billing/checkout-session', { pricingPlanId, pricingInterval }),
8 | z.object({
9 | redirect: z.string().url().optional(),
10 | invoice: invoiceSchema.optional()
11 | })
12 | )
13 |
14 | export default createCheckoutSession
15 |
--------------------------------------------------------------------------------
/src/api/createDataExport.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod'
2 | import { dataExportSchema } from '../entities/dataExport'
3 | import api from './api'
4 | import makeValidatedRequest from './makeValidatedRequest'
5 |
6 | const createDataExport = makeValidatedRequest(
7 | (gameId, entities) => api.post(`/games/${gameId}/data-exports`, { entities }),
8 | z.object({
9 | dataExport: dataExportSchema
10 | })
11 | )
12 |
13 | export default createDataExport
14 |
--------------------------------------------------------------------------------
/src/api/createDemo.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod'
2 | import api from './api'
3 | import makeValidatedRequest from './makeValidatedRequest'
4 | import { userSchema } from '../entities/user'
5 |
6 | const createDemo = makeValidatedRequest(
7 | () => api.post('/public/demo'),
8 | z.object({
9 | accessToken: z.string(),
10 | user: userSchema
11 | })
12 | )
13 |
14 | export default createDemo
15 |
--------------------------------------------------------------------------------
/src/api/createFeedbackCategory.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod'
2 | import { GameFeedbackCategory, gameFeedbackCategorySchema } from '../entities/gameFeedbackCategory'
3 | import api from './api'
4 | import makeValidatedRequest from './makeValidatedRequest'
5 |
6 | type Data = Pick
7 |
8 | const createFeedbackCategory = makeValidatedRequest(
9 | (gameId: number, data: Data) => api.post(`/games/${gameId}/game-feedback/categories`, data),
10 | z.object({
11 | feedbackCategory: gameFeedbackCategorySchema
12 | })
13 | )
14 |
15 | export default createFeedbackCategory
16 |
--------------------------------------------------------------------------------
/src/api/createGame.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod'
2 | import { gameSchema } from '../entities/game'
3 | import api from './api'
4 | import makeValidatedRequest from './makeValidatedRequest'
5 |
6 | const createGame = makeValidatedRequest(
7 | (name: string) => api.post('/games', { name }),
8 | z.object({
9 | game: gameSchema
10 | })
11 | )
12 |
13 | export default createGame
14 |
--------------------------------------------------------------------------------
/src/api/createGroup.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod'
2 | import { PlayerGroup, playerGroupSchema } from '../entities/playerGroup'
3 | import api from './api'
4 | import makeValidatedRequest from './makeValidatedRequest'
5 |
6 | type Data = Pick
7 |
8 | const createGroup = makeValidatedRequest(
9 | (gameId: number, data: Data) => api.post(`/games/${gameId}/player-groups`, data),
10 | z.object({
11 | group: playerGroupSchema
12 | })
13 | )
14 |
15 | export default createGroup
16 |
--------------------------------------------------------------------------------
/src/api/createInvite.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod'
2 | import { UserType } from '../entities/user'
3 | import api from './api'
4 | import makeValidatedRequest from './makeValidatedRequest'
5 | import { inviteSchema } from '../entities/invite'
6 |
7 | const createInvite = makeValidatedRequest(
8 | (email: string, type: UserType) => api.post('/invites', { email, type }),
9 | z.object({
10 | invite: inviteSchema
11 | })
12 | )
13 |
14 | export default createInvite
15 |
--------------------------------------------------------------------------------
/src/api/createLeaderboard.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod'
2 | import { Leaderboard, leaderboardSchema } from '../entities/leaderboard'
3 | import api from './api'
4 | import makeValidatedRequest from './makeValidatedRequest'
5 |
6 | type Data = Pick
7 |
8 | const createLeaderboard = makeValidatedRequest(
9 | (gameId: number, data: Data) => api.post(`/games/${gameId}/leaderboards`, data),
10 | z.object({
11 | leaderboard: leaderboardSchema
12 | })
13 | )
14 |
15 | export default createLeaderboard
16 |
--------------------------------------------------------------------------------
/src/api/createPortalSession.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod'
2 | import api from './api'
3 | import makeValidatedRequest from './makeValidatedRequest'
4 |
5 | const createPortalSession = makeValidatedRequest(
6 | () => api.post('/billing/portal-session'),
7 | z.object({
8 | redirect: z.string().url()
9 | })
10 | )
11 |
12 | export default createPortalSession
13 |
--------------------------------------------------------------------------------
/src/api/createRecoveryCodes.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod'
2 | import api from './api'
3 | import makeValidatedRequest from './makeValidatedRequest'
4 |
5 | const createRecoveryCodes = makeValidatedRequest(
6 | (password: string) => api.post('/users/2fa/recovery_codes/create', { password }),
7 | z.object({
8 | recoveryCodes: z.array(z.string())
9 | })
10 | )
11 |
12 | export default createRecoveryCodes
13 |
--------------------------------------------------------------------------------
/src/api/createStat.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod'
2 | import { GameStat, gameStatSchema } from '../entities/gameStat'
3 | import api from './api'
4 | import makeValidatedRequest from './makeValidatedRequest'
5 |
6 | type Data = Pick
15 |
16 | const createStat = makeValidatedRequest(
17 | (gameId: number, data: Data) => api.post(`/games/${gameId}/game-stats`, data),
18 | z.object({
19 | stat: gameStatSchema
20 | })
21 | )
22 |
23 | export default createStat
24 |
--------------------------------------------------------------------------------
/src/api/deleteAPIKey.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod'
2 | import api from './api'
3 | import makeValidatedRequest from './makeValidatedRequest'
4 |
5 | const deleteAPIKey = makeValidatedRequest(
6 | (gameId: number, apiKeyId: number) => api.delete(`/games/${gameId}/api-keys/${apiKeyId}`),
7 | z.literal('')
8 | )
9 |
10 | export default deleteAPIKey
11 |
--------------------------------------------------------------------------------
/src/api/deleteChannel.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod'
2 | import api from './api'
3 | import makeValidatedRequest from './makeValidatedRequest'
4 |
5 | const deleteChannel = makeValidatedRequest(
6 | (gameId: number, channelId: number) => api.delete(`/games/${gameId}/game-channels/${channelId}`),
7 | z.literal('')
8 | )
9 |
10 | export default deleteChannel
11 |
--------------------------------------------------------------------------------
/src/api/deleteFeedbackCategory.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod'
2 | import api from './api'
3 | import makeValidatedRequest from './makeValidatedRequest'
4 |
5 | const deleteFeedbackCategory = makeValidatedRequest(
6 | (gameId: number, feedbackCategoryId: number) => api.delete(`/games/${gameId}/game-feedback/categories/${feedbackCategoryId}`),
7 | z.literal('')
8 | )
9 |
10 | export default deleteFeedbackCategory
11 |
--------------------------------------------------------------------------------
/src/api/deleteGroup.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod'
2 | import api from './api'
3 | import makeValidatedRequest from './makeValidatedRequest'
4 |
5 | const deleteGroup = makeValidatedRequest(
6 | (gameId: number, groupId: string) => api.delete(`/games/${gameId}/player-groups/${groupId}`),
7 | z.literal('')
8 | )
9 |
10 | export default deleteGroup
11 |
--------------------------------------------------------------------------------
/src/api/deleteLeaderboard.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod'
2 | import api from './api'
3 | import makeValidatedRequest from './makeValidatedRequest'
4 |
5 | const deleteLeaderboard = makeValidatedRequest(
6 | (gameId: number, leaderboardId: number) => api.delete(`/games/${gameId}/leaderboards/${leaderboardId}`),
7 | z.literal('')
8 | )
9 |
10 | export default deleteLeaderboard
11 |
--------------------------------------------------------------------------------
/src/api/deleteStat.ts:
--------------------------------------------------------------------------------
1 | import api from './api'
2 | import { z } from 'zod'
3 | import makeValidatedRequest from './makeValidatedRequest'
4 |
5 | const deleteStat = makeValidatedRequest(
6 | (gameId: number, statId: number) => api.delete(`/games/${gameId}/game-stats/${statId}`),
7 | z.literal('')
8 | )
9 |
10 | export default deleteStat
11 |
--------------------------------------------------------------------------------
/src/api/disable2FA.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod'
2 | import api from './api'
3 | import makeValidatedRequest from './makeValidatedRequest'
4 | import { userSchema } from '../entities/user'
5 |
6 | const disable2FA = makeValidatedRequest(
7 | (password) => api.post('/users/2fa/disable', { password }),
8 | z.object({
9 | user: userSchema
10 | })
11 | )
12 |
13 | export default disable2FA
14 |
--------------------------------------------------------------------------------
/src/api/disableIntegration.ts:
--------------------------------------------------------------------------------
1 | import api from './api'
2 | import { z } from 'zod'
3 | import makeValidatedRequest from './makeValidatedRequest'
4 |
5 | const disableIntegration = makeValidatedRequest(
6 | (gameId: number, integrationId: number) => api.delete(`/games/${gameId}/integrations/${integrationId}`),
7 | z.literal('')
8 | )
9 |
10 | export default disableIntegration
11 |
--------------------------------------------------------------------------------
/src/api/enable2FA.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod'
2 | import api from './api'
3 | import makeValidatedRequest from './makeValidatedRequest'
4 |
5 | const enable2FA = makeValidatedRequest(
6 | () => api.get('/users/2fa/enable'),
7 | z.object({
8 | qr: z.string()
9 | })
10 | )
11 |
12 | export default enable2FA
13 |
--------------------------------------------------------------------------------
/src/api/enableIntegration.ts:
--------------------------------------------------------------------------------
1 | import api from './api'
2 | import { z } from 'zod'
3 | import makeValidatedRequest from './makeValidatedRequest'
4 | import { Integration, integrationSchema } from '../entities/integration'
5 |
6 | const enableIntegration = makeValidatedRequest(
7 | (gameId: number, data: Pick) => api.post(`/games/${gameId}/integrations`, data),
8 | z.object({
9 | integration: integrationSchema
10 | })
11 | )
12 |
13 | export default enableIntegration
14 |
--------------------------------------------------------------------------------
/src/api/findLeaderboard.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod'
2 | import { leaderboardSchema } from '../entities/leaderboard'
3 | import api from './api'
4 | import makeValidatedRequest from './makeValidatedRequest'
5 |
6 | const findLeaderboard = makeValidatedRequest(
7 | (gameId: number, internalName: string) => api.get(`/games/${gameId}/leaderboards/search?internalName=${internalName}`),
8 | z.object({
9 | leaderboard: leaderboardSchema
10 | })
11 | )
12 |
13 | export default findLeaderboard
14 |
--------------------------------------------------------------------------------
/src/api/findPlayer.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod'
2 | import api from './api'
3 | import makeValidatedRequest from './makeValidatedRequest'
4 | import { playerSchema } from '../entities/player'
5 |
6 | const findPlayer = makeValidatedRequest(
7 | (gameId: number, playerId: string) => api.get(`/games/${gameId}/players?search=${playerId}`),
8 | z.object({
9 | players: z.array(playerSchema)
10 | })
11 | )
12 |
13 | export default findPlayer
14 |
--------------------------------------------------------------------------------
/src/api/getInvite.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod'
2 | import { inviteSchema } from '../entities/invite'
3 | import api from './api'
4 | import makeValidatedRequest from './makeValidatedRequest'
5 |
6 | const getInvite = makeValidatedRequest(
7 | (token: string) => api.get(`/public/invites/${token}`),
8 | z.object({
9 | invite: inviteSchema
10 | })
11 | )
12 |
13 | export default getInvite
14 |
--------------------------------------------------------------------------------
/src/api/login.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod'
2 | import api from './api'
3 | import makeValidatedRequest from './makeValidatedRequest'
4 | import { userSchema } from '../entities/user'
5 |
6 | const twoFactorAuthResponseSchema = z.object({
7 | twoFactorAuthRequired: z.literal(true),
8 | userId: z.number()
9 | })
10 |
11 | const loginSuccessResponseSchema = z.object({
12 | accessToken: z.string(),
13 | user: userSchema
14 | })
15 |
16 | const login = makeValidatedRequest(
17 | (data: { email: string, password: string }) => api.post('/public/users/login', data),
18 | z.union([twoFactorAuthResponseSchema, loginSuccessResponseSchema])
19 | )
20 |
21 | export default login
22 |
--------------------------------------------------------------------------------
/src/api/logout.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod'
2 | import api from './api'
3 | import makeValidatedRequest from './makeValidatedRequest'
4 |
5 | const logout = makeValidatedRequest(
6 | () => api.post('/users/logout'),
7 | z.literal('')
8 | )
9 |
10 | export default logout
11 |
--------------------------------------------------------------------------------
/src/api/makeValidatedGetRequest.ts:
--------------------------------------------------------------------------------
1 | import { ZodType } from 'zod'
2 | import api from './api'
3 | import makeValidatedRequest from './makeValidatedRequest'
4 |
5 | export default function makeValidatedGetRequest(url: string, validator: T) {
6 | return makeValidatedRequest(() => api.get(url), validator)()
7 | }
8 |
--------------------------------------------------------------------------------
/src/api/makeValidatedRequest.ts:
--------------------------------------------------------------------------------
1 | import { captureException } from '@sentry/react'
2 | import { AxiosResponse } from 'axios'
3 | import { z, ZodError } from 'zod'
4 | import type { ZodType } from 'zod'
5 |
6 | export default function makeValidatedRequest(
7 | doFetch: (...args: A) => Promise,
8 | validator: T
9 | ): (...args: A) => Promise> {
10 | return async (...args) => {
11 | const res = await doFetch(...args)
12 | const rawResult = res.data
13 |
14 | try {
15 | return await validator.parseAsync(rawResult)
16 | } catch (parseError) {
17 | if (parseError instanceof ZodError) {
18 | console.error(`Response body (${res.request.responseURL}) is invalid`, parseError.errors.map((error) => ({
19 | ...error,
20 | path: error.path.join('.')
21 | })))
22 | }
23 |
24 | captureException(parseError)
25 | return rawResult
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/api/recoverAccount.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod'
2 | import api from './api'
3 | import makeValidatedRequest from './makeValidatedRequest'
4 | import { userSchema } from '../entities/user'
5 |
6 | const recoverAccount = makeValidatedRequest(
7 | (code: string, userId: number) => api.post('/public/users/2fa/recover', { code, userId }),
8 | z.object({
9 | accessToken: z.string(),
10 | user: userSchema,
11 | newRecoveryCodes: z.array(z.string()).optional()
12 | })
13 | )
14 |
15 | export default recoverAccount
16 |
--------------------------------------------------------------------------------
/src/api/refreshAccess.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | import AuthService from '../services/AuthService'
3 | import { apiConfig } from './api'
4 | import makeValidatedRequest from './makeValidatedRequest'
5 | import { z } from 'zod'
6 | import { userSchema } from '../entities/user'
7 |
8 | const refreshAccess = makeValidatedRequest(
9 | () => {
10 | const api = axios.create(apiConfig)
11 |
12 | api.interceptors.response.use((response) => {
13 | return response
14 | }, (error) => {
15 | if (AuthService.getToken() && error.response?.status === 401) AuthService.reload()
16 | return Promise.reject(error)
17 | })
18 |
19 | return api.get('/public/users/refresh')
20 | },
21 | z.object({
22 | accessToken: z.string(),
23 | user: userSchema
24 | })
25 | )
26 |
27 | export default refreshAccess
28 |
--------------------------------------------------------------------------------
/src/api/register.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod'
2 | import api from './api'
3 | import makeValidatedRequest from './makeValidatedRequest'
4 | import { userSchema } from '../entities/user'
5 |
6 | export type Data = {
7 | email: string
8 | password: string
9 | organisationName?: string
10 | username: string
11 | inviteToken?: string
12 | }
13 |
14 | const register = makeValidatedRequest(
15 | (data: Data) => api.post('/public/users/register', data),
16 | z.object({
17 | accessToken: z.string(),
18 | user: userSchema
19 | })
20 | )
21 |
22 | export default register
23 |
--------------------------------------------------------------------------------
/src/api/requestNewPassword.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod'
2 | import api from './api'
3 | import makeValidatedRequest from './makeValidatedRequest'
4 |
5 | const requestNewPassword = makeValidatedRequest(
6 | (email: string) => api.post('/public/users/forgot_password', { email }),
7 | z.literal('')
8 | )
9 |
10 | export default requestNewPassword
11 |
--------------------------------------------------------------------------------
/src/api/resetPassword.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod'
2 | import api from './api'
3 | import makeValidatedRequest from './makeValidatedRequest'
4 |
5 | const resetPassword = makeValidatedRequest(
6 | (token: string, password: string) => api.post('/public/users/reset_password', { token, password }),
7 | z.literal('')
8 | )
9 |
10 | export default resetPassword
11 |
--------------------------------------------------------------------------------
/src/api/syncLeaderboards.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod'
2 | import api from './api'
3 | import makeValidatedRequest from './makeValidatedRequest'
4 |
5 | const syncLeaderboards = makeValidatedRequest(
6 | (gameId: number, integrationId: number) => api.post(`/games/${gameId}/integrations/${integrationId}/sync-leaderboards`),
7 | z.literal('')
8 | )
9 |
10 | export default syncLeaderboards
11 |
--------------------------------------------------------------------------------
/src/api/syncStats.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod'
2 | import api from './api'
3 | import makeValidatedRequest from './makeValidatedRequest'
4 |
5 | const syncStats = makeValidatedRequest(
6 | (gameId: number, integrationId: number) => api.post(`/games/${gameId}/integrations/${integrationId}/sync-stats`),
7 | z.literal('')
8 | )
9 |
10 | export default syncStats
11 |
--------------------------------------------------------------------------------
/src/api/toggledPinnedGroup.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod'
2 | import api from './api'
3 | import makeValidatedRequest from './makeValidatedRequest'
4 |
5 | const togglePinnedGroup = makeValidatedRequest(
6 | (gameId: number, groupId: string, pinned: boolean) => api.put(`/games/${gameId}/player-groups/${groupId}/toggle-pinned`, { pinned }),
7 | z.literal('')
8 | )
9 |
10 | export default togglePinnedGroup
11 |
--------------------------------------------------------------------------------
/src/api/updateAPIKey.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod'
2 | import { apiKeySchema } from '../entities/apiKey'
3 | import api from './api'
4 | import makeValidatedRequest from './makeValidatedRequest'
5 |
6 | const updateAPIKey = makeValidatedRequest(
7 | (gameId: number, apiKeyId: number, data: { scopes: string[] }) => api.put(`/games/${gameId}/api-keys/${apiKeyId}`, data),
8 | z.object({
9 | apiKey: apiKeySchema
10 | })
11 | )
12 |
13 | export default updateAPIKey
14 |
--------------------------------------------------------------------------------
/src/api/updateChannel.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod'
2 | import api from './api'
3 | import { GameChannel, gameChannelSchema } from '../entities/gameChannels'
4 | import makeValidatedRequest from './makeValidatedRequest'
5 |
6 | type Data = Pick & { ownerAliasId: number | null }
7 |
8 | const updateChannel = makeValidatedRequest(
9 | (gameId: number, channelId: number, data: Data) => api.put(`/games/${gameId}/game-channels/${channelId}`, data),
10 | z.object({
11 | channel: gameChannelSchema
12 | })
13 | )
14 |
15 | export default updateChannel
16 |
--------------------------------------------------------------------------------
/src/api/updateFeedbackCategory.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod'
2 | import api from './api'
3 | import makeValidatedRequest from './makeValidatedRequest'
4 | import { GameFeedbackCategory, gameFeedbackCategorySchema } from '../entities/gameFeedbackCategory'
5 |
6 | type Data = Pick
7 |
8 | const updateFeedbackCategory = makeValidatedRequest(
9 | (gameId: number, feedbackCategoryId: number, data: Data) => api.put(`/games/${gameId}/game-feedback/categories/${feedbackCategoryId}`, data),
10 | z.object({
11 | feedbackCategory: gameFeedbackCategorySchema
12 | })
13 | )
14 |
15 | export default updateFeedbackCategory
16 |
--------------------------------------------------------------------------------
/src/api/updateGame.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod'
2 | import { gameSchema } from '../entities/game'
3 | import api from './api'
4 | import makeValidatedRequest from './makeValidatedRequest'
5 | import { Prop } from '../entities/prop'
6 |
7 | type Data = {
8 | name?: string
9 | props?: Prop[]
10 | purgeDevPlayers?: boolean
11 | purgeLivePlayers?: boolean
12 | website?: string | null
13 | }
14 |
15 | const updateGame = makeValidatedRequest(
16 | (gameId: number, data: Data) => api.patch(`/games/${gameId}`, data),
17 | z.object({
18 | game: gameSchema
19 | })
20 | )
21 |
22 | export default updateGame
23 |
--------------------------------------------------------------------------------
/src/api/updateGroup.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod'
2 | import { PlayerGroup, playerGroupSchema } from '../entities/playerGroup'
3 | import api from './api'
4 | import makeValidatedRequest from './makeValidatedRequest'
5 |
6 | type Data = Pick
7 |
8 | const updateGroup = makeValidatedRequest(
9 | (gameId: number, groupId: string, data: Data) => api.put(`/games/${gameId}/player-groups/${groupId}`, data),
10 | z.object({
11 | group: playerGroupSchema
12 | })
13 | )
14 |
15 | export default updateGroup
16 |
--------------------------------------------------------------------------------
/src/api/updateIntegration.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod'
2 | import api from './api'
3 | import makeValidatedRequest from './makeValidatedRequest'
4 | import { IntegrationConfig, integrationSchema } from '../entities/integration'
5 |
6 | const updateIntegration = makeValidatedRequest(
7 | (gameId: number, integrationId: number, data: { config: Partial }) =>
8 | api.patch(`/games/${gameId}/integrations/${integrationId}`, data),
9 | z.object({
10 | integration: integrationSchema
11 | })
12 | )
13 |
14 | export default updateIntegration
15 |
--------------------------------------------------------------------------------
/src/api/updateLeaderboard.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod'
2 | import api from './api'
3 | import { Leaderboard, leaderboardSchema } from '../entities/leaderboard'
4 | import makeValidatedRequest from './makeValidatedRequest'
5 |
6 | type Data = Pick
7 |
8 | const updateLeaderboard = makeValidatedRequest(
9 | (gameId: number, leaderboardId: number, data: Data) => api.put(`/games/${gameId}/leaderboards/${leaderboardId}`, data),
10 | z.object({
11 | leaderboard: leaderboardSchema
12 | })
13 | )
14 |
15 | export default updateLeaderboard
16 |
--------------------------------------------------------------------------------
/src/api/updateLeaderboardEntry.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod'
2 | import { leaderboardEntrySchema } from '../entities/leaderboardEntry'
3 | import api from './api'
4 | import makeValidatedRequest from './makeValidatedRequest'
5 |
6 | const updateLeaderboardEntry = makeValidatedRequest(
7 | (gameId: number, leaderboardId: number, entryId: number, data: { hidden?: boolean, newScore?: number }) =>
8 | api.patch(`/games/${gameId}/leaderboards/${leaderboardId}/entries/${entryId}`, data),
9 | z.object({
10 | entry: leaderboardEntrySchema
11 | })
12 | )
13 |
14 | export default updateLeaderboardEntry
15 |
--------------------------------------------------------------------------------
/src/api/updatePlayer.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod'
2 | import { playerSchema } from '../entities/player'
3 | import api from './api'
4 | import makeValidatedRequest from './makeValidatedRequest'
5 | import { Prop } from '../entities/prop'
6 |
7 | const updatePlayer = makeValidatedRequest(
8 | (gameId: number, playerId: string, data: { props: Prop[] }) => api.patch(`/games/${gameId}/players/${playerId}`, data),
9 | z.object({
10 | player: playerSchema
11 | })
12 | )
13 |
14 | export default updatePlayer
15 |
--------------------------------------------------------------------------------
/src/api/updateStat.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod'
2 | import { GameStat, gameStatSchema } from '../entities/gameStat'
3 | import api from './api'
4 | import makeValidatedRequest from './makeValidatedRequest'
5 |
6 | type Data = Pick
15 |
16 | const updateStat = makeValidatedRequest(
17 | (gameId: number, statId: number, data: Data) => api.put(`/games/${gameId}/game-stats/${statId}`, data),
18 | z.object({
19 | stat: gameStatSchema
20 | })
21 | )
22 |
23 | export default updateStat
24 |
--------------------------------------------------------------------------------
/src/api/updateStatValue.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod'
2 | import api from './api'
3 | import makeValidatedRequest from './makeValidatedRequest'
4 | import { playerGameStatSchema } from '../entities/playerGameStat'
5 |
6 | const updateStatValue = makeValidatedRequest(
7 | (gameId: number, statId: number, playerStatId: number, newValue: number) => {
8 | return api.patch(`/games/${gameId}/game-stats/${statId}/player-stats/${playerStatId}`, { newValue })
9 | },
10 | z.object({
11 | playerStat: playerGameStatSchema
12 | })
13 | )
14 |
15 | export default updateStatValue
16 |
--------------------------------------------------------------------------------
/src/api/useAPIKeys.ts:
--------------------------------------------------------------------------------
1 | import useSWR from 'swr'
2 | import buildError from '../utils/buildError'
3 | import { Game } from '../entities/game'
4 | import { z } from 'zod'
5 | import { apiKeySchema } from '../entities/apiKey'
6 | import makeValidatedGetRequest from './makeValidatedGetRequest'
7 |
8 | export default function useAPIKeys(activeGame: Game) {
9 | const fetcher = async ([url]: [string]) => {
10 | const { apiKeys } = await makeValidatedGetRequest(url, z.object({
11 | apiKeys: z.array(apiKeySchema)
12 | }))
13 | const { scopes } = await makeValidatedGetRequest(`${url}/scopes`, z.object({
14 | scopes: z.record(z.array(z.string()))
15 | }))
16 |
17 | return {
18 | apiKeys,
19 | scopes
20 | }
21 | }
22 |
23 | const { data, error, mutate } = useSWR(
24 | [`/games/${activeGame.id}/api-keys`],
25 | fetcher
26 | )
27 |
28 | return {
29 | apiKeys: data?.apiKeys ?? [],
30 | scopes: data?.scopes ?? {},
31 | loading: !data && !error,
32 | error: error && buildError(error),
33 | mutate
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/api/useChannels.ts:
--------------------------------------------------------------------------------
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 { gameChannelSchema } from '../entities/gameChannels'
7 |
8 | export const channelsSchema = z.object({
9 | channels: z.array(gameChannelSchema),
10 | count: z.number(),
11 | itemsPerPage: z.number(),
12 | isLastPage: z.boolean()
13 | })
14 |
15 | export default function useChannels(activeGame: Game, 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()}`, channelsSchema)
19 | return res
20 | }
21 |
22 | const { data, error, mutate } = useSWR(
23 | [`/games/${activeGame.id}/game-channels`, search, page],
24 | fetcher
25 | )
26 |
27 | return {
28 | channels: data?.channels ?? [],
29 | count: data?.count,
30 | itemsPerPage: data?.itemsPerPage,
31 | loading: !data && !error,
32 | error: error && buildError(error),
33 | mutate
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/api/useDataExportEntities.ts:
--------------------------------------------------------------------------------
1 | import useSWR from 'swr'
2 | import buildError from '../utils/buildError'
3 | import { Game } from '../entities/game'
4 | import { z } from 'zod'
5 | import { DataExportAvailableEntities } from '../entities/dataExport'
6 | import makeValidatedGetRequest from './makeValidatedGetRequest'
7 |
8 | export default function useDataExportEntities(activeGame: Game) {
9 | const fetcher = async ([url]: [string]) => {
10 | const res = await makeValidatedGetRequest(url, z.object({
11 | entities: z.array(z.nativeEnum(DataExportAvailableEntities))
12 | }))
13 |
14 | return res
15 | }
16 |
17 | const { data, error } = useSWR(
18 | [`/games/${activeGame.id}/data-exports/entities`],
19 | fetcher
20 | )
21 |
22 | return {
23 | entities: data?.entities ?? [],
24 | loading: !data && !error,
25 | error: error && buildError(error)
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/api/useDataExports.ts:
--------------------------------------------------------------------------------
1 | import useSWR from 'swr'
2 | import buildError from '../utils/buildError'
3 | import { Game } from '../entities/game'
4 | import makeValidatedGetRequest from './makeValidatedGetRequest'
5 | import { dataExportSchema } from '../entities/dataExport'
6 | import { z } from 'zod'
7 |
8 | const useDataExports = (activeGame: Game, createdExportId: number | null) => {
9 | const fetcher = async ([url]: [string]) => {
10 | const res = await makeValidatedGetRequest(url, z.object({
11 | dataExports: z.array(dataExportSchema)
12 | }))
13 |
14 | return res
15 | }
16 |
17 | const { data, error } = useSWR(
18 | [`/games/${activeGame.id}/data-exports`, createdExportId],
19 | fetcher
20 | )
21 |
22 | return {
23 | dataExports: data?.dataExports ?? [],
24 | loading: !data && !error,
25 | error: error && buildError(error)
26 | }
27 | }
28 |
29 | export default useDataExports
30 |
--------------------------------------------------------------------------------
/src/api/useEventBreakdown.ts:
--------------------------------------------------------------------------------
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 { eventsVisualisationPayloadSchema } from './useEvents'
7 |
8 | export default function useEventBreakdown(activeGame: Game, eventName: string, startDate: string, endDate: string) {
9 | const fetcher = async ([url]: [string]) => {
10 | const qs = new URLSearchParams({
11 | eventName,
12 | startDate,
13 | endDate
14 | }).toString()
15 |
16 | const res = await makeValidatedGetRequest(`${url}?${qs}`, z.object({
17 | events: z.record(
18 | z.array(eventsVisualisationPayloadSchema)
19 | ),
20 | eventNames: z.array(z.string())
21 | }))
22 |
23 | return res
24 | }
25 |
26 | const { data, error } = useSWR(
27 | activeGame && eventName && startDate && endDate ? [`/games/${activeGame.id}/events/breakdown`, eventName, startDate, endDate] : null,
28 | fetcher
29 | )
30 |
31 | return {
32 | events: data?.events,
33 | eventNames: data?.eventNames ?? [],
34 | loading: !data && !error,
35 | error: error && buildError(error)
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/api/useEvents.ts:
--------------------------------------------------------------------------------
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 |
7 | export const eventsVisualisationPayloadSchema = z.object({
8 | name: z.string(),
9 | date: z.number(),
10 | count: z.number(),
11 | change: z.number()
12 | })
13 |
14 | export default function useEvents(activeGame: Game, startDate: string, endDate: string) {
15 | const fetcher = async ([url]: [string]) => {
16 | const qs = new URLSearchParams({
17 | startDate,
18 | endDate
19 | }).toString()
20 |
21 | const res = await makeValidatedGetRequest(`${url}?${qs}`, z.object({
22 | events: z.record(
23 | z.array(eventsVisualisationPayloadSchema)
24 | ),
25 | eventNames: z.array(z.string())
26 | }))
27 |
28 | return res
29 | }
30 |
31 | const { data, error } = useSWR(
32 | activeGame && startDate && endDate ? [`/games/${activeGame.id}/events`, startDate, endDate] : null,
33 | fetcher
34 | )
35 |
36 | return {
37 | events: data?.events,
38 | eventNames: data?.eventNames ?? [],
39 | loading: !data && !error,
40 | error: error && buildError(error)
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/api/useFeedback.ts:
--------------------------------------------------------------------------------
1 | import useSWR from 'swr'
2 | import buildError from '../utils/buildError'
3 | import { Game } from '../entities/game'
4 | import makeValidatedGetRequest from './makeValidatedGetRequest'
5 | import { gameFeedbackSchema } from '../entities/gameFeedback'
6 | import { z } from 'zod'
7 |
8 | export default function useFeedback(activeGame: Game, feedbackCategoryInternalName: string | null, search: string, page: number) {
9 | const fetcher = async ([url]: [string]) => {
10 | const params = new URLSearchParams({ page: String(page), search })
11 | if (feedbackCategoryInternalName) {
12 | params.append('feedbackCategoryInternalName', feedbackCategoryInternalName)
13 | }
14 |
15 | const res = await makeValidatedGetRequest(`${url}?${params.toString()}`, z.object({
16 | feedback: z.array(gameFeedbackSchema),
17 | count: z.number(),
18 | itemsPerPage: z.number()
19 | }))
20 |
21 | return res
22 | }
23 |
24 | const { data, error } = useSWR(
25 | [`/games/${activeGame.id}/game-feedback`, feedbackCategoryInternalName, search, page],
26 | fetcher
27 | )
28 |
29 | return {
30 | feedback: data?.feedback ?? [],
31 | count: data?.count,
32 | itemsPerPage: data?.itemsPerPage,
33 | loading: !data && !error,
34 | error: error && buildError(error)
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/api/useFeedbackCategories.ts:
--------------------------------------------------------------------------------
1 | import useSWR from 'swr'
2 | import buildError from '../utils/buildError'
3 | import { Game } from '../entities/game'
4 | import makeValidatedGetRequest from './makeValidatedGetRequest'
5 | import { gameFeedbackCategorySchema } from '../entities/gameFeedbackCategory'
6 | import { z } from 'zod'
7 |
8 | export default function useFeedbackCategories(activeGame: Game) {
9 | const fetcher = async ([url]: [string]) => {
10 | const res = await makeValidatedGetRequest(url, z.object({
11 | feedbackCategories: z.array(gameFeedbackCategorySchema)
12 | }))
13 |
14 | return res
15 | }
16 |
17 | const { data, error, mutate } = useSWR(
18 | [`/games/${activeGame.id}/game-feedback/categories`],
19 | fetcher
20 | )
21 |
22 | return {
23 | feedbackCategories: data?.feedbackCategories ?? [],
24 | loading: !data && !error,
25 | error: error && buildError(error),
26 | mutate
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/api/useGameActivities.ts:
--------------------------------------------------------------------------------
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 { gameActivitySchema } from '../entities/gameActivity'
7 |
8 | export default function useGameActivities(activeGame: Game) {
9 | const fetcher = async ([url]: [string]) => {
10 | const res = await makeValidatedGetRequest(url, z.object({
11 | activities: z.array(gameActivitySchema)
12 | }))
13 |
14 | return res
15 | }
16 |
17 | const { data, error } = useSWR(
18 | [`/games/${activeGame.id}/game-activities`],
19 | fetcher
20 | )
21 |
22 | return {
23 | activities: data?.activities ?? [],
24 | loading: !data && !error,
25 | error: error && buildError(error)
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/api/useGameSettings.ts:
--------------------------------------------------------------------------------
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 |
7 | export default function useGameSettings(activeGame: Game) {
8 | const fetcher = async ([url]: [string]) => {
9 | const res = await makeValidatedGetRequest(url, z.object({
10 | settings: z.object({
11 | purgeDevPlayers: z.boolean(),
12 | purgeLivePlayers: z.boolean(),
13 | website: z.string().nullable()
14 | })
15 | }))
16 |
17 | return res
18 | }
19 |
20 | const { data, error } = useSWR(
21 | [`/games/${activeGame.id}/settings`],
22 | fetcher
23 | )
24 |
25 | return {
26 | settings: data?.settings,
27 | loading: !data && !error,
28 | error: error && buildError(error)
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/api/useGroupPreviewCount.ts:
--------------------------------------------------------------------------------
1 | import useSWR from 'swr'
2 | import buildError from '../utils/buildError'
3 | import prepareRule from '../utils/group-rules/prepareRule'
4 | import isGroupRuleValid from '../utils/group-rules/isGroupRuleValid'
5 | import { useMemo } from 'react'
6 | import { Game } from '../entities/game'
7 | import { UnpackedGroupRule } from '../modals/groups/GroupDetails'
8 | import { PlayerGroupRuleMode } from '../entities/playerGroup'
9 | import makeValidatedGetRequest from './makeValidatedGetRequest'
10 | import { z } from 'zod'
11 |
12 | export default function useGroupPreviewCount(activeGame: Game, ruleMode: PlayerGroupRuleMode, rules: UnpackedGroupRule[]) {
13 | const fetcher = async ([url]: [string]) => {
14 | const qs = new URLSearchParams({
15 | ruleMode,
16 | rules: JSON.stringify(rules.map(prepareRule))
17 | }).toString()
18 |
19 | const res = await makeValidatedGetRequest(`${url}?${qs}`, z.object({
20 | count: z.number()
21 | }))
22 |
23 | return res
24 | }
25 |
26 | const allValid = useMemo(() => {
27 | return rules.every(isGroupRuleValid)
28 | }, [rules])
29 |
30 | const { data, error } = useSWR(
31 | activeGame && allValid ? [`games/${activeGame.id}/player-groups/preview-count`, ruleMode, JSON.stringify(rules)] : null,
32 | fetcher
33 | )
34 |
35 | return {
36 | count: data?.count,
37 | loading: allValid && !data && !error,
38 | error: error && buildError(error)
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/api/useGroupRules.ts:
--------------------------------------------------------------------------------
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 { availablePlayerGroupFieldSchema, playerGroupRuleOptionSchema } from '../entities/playerGroup'
7 |
8 | export default function useGroupRules(activeGame: Game) {
9 | const fetcher = async ([url]: [string]) => {
10 | const res = await makeValidatedGetRequest(url, z.object({
11 | availableRules: z.array(playerGroupRuleOptionSchema),
12 | availableFields: z.array(availablePlayerGroupFieldSchema)
13 | }))
14 |
15 | return res
16 | }
17 |
18 | const { data, error } = useSWR(
19 | [`games/${activeGame.id}/player-groups/rules`],
20 | fetcher
21 | )
22 |
23 | return {
24 | availableRules: data?.availableRules ?? [],
25 | availableFields: data?.availableFields ?? [],
26 | loading: !data && !error,
27 | error: error && buildError(error)
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/api/useGroups.ts:
--------------------------------------------------------------------------------
1 | import useSWR from 'swr'
2 | import buildError from '../utils/buildError'
3 | import { Game } from '../entities/game'
4 | import makeValidatedGetRequest from './makeValidatedGetRequest'
5 | import { playerGroupSchema } from '../entities/playerGroup'
6 | import { z } from 'zod'
7 |
8 | export default function useGroups(activeGame: Game) {
9 | const fetcher = async ([url]: [string]) => {
10 | const res = await makeValidatedGetRequest(url, z.object({
11 | groups: z.array(playerGroupSchema)
12 | }))
13 |
14 | return res
15 | }
16 |
17 | const { data, error, mutate } = useSWR(
18 | [`games/${activeGame.id}/player-groups`],
19 | fetcher
20 | )
21 |
22 | return {
23 | groups: data?.groups ?? [],
24 | loading: !data && !error,
25 | error: error && buildError(error),
26 | mutate
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/api/useIntegrations.ts:
--------------------------------------------------------------------------------
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 { integrationSchema } from '../entities/integration'
7 |
8 | export default function useIntegrations(activeGame: Game) {
9 | const fetcher = async ([url]: [string]) => {
10 | const res = await makeValidatedGetRequest(url, z.object({
11 | integrations: z.array(integrationSchema)
12 | }))
13 |
14 | return res
15 | }
16 |
17 | const { data, error, mutate } = useSWR(
18 | [`/games/${activeGame.id}/integrations`],
19 | fetcher
20 | )
21 |
22 | return {
23 | integrations: data?.integrations ?? [],
24 | loading: !data && !error,
25 | error: error && buildError(error),
26 | mutate
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/api/useLeaderboardEntries.ts:
--------------------------------------------------------------------------------
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 { leaderboardEntrySchema } from '../entities/leaderboardEntry'
7 |
8 | export default function useLeaderboardEntries(activeGame: Game, leaderboardId: number | undefined, page: number, withDeleted: boolean = false) {
9 | const fetcher = async ([url]: [string]) => {
10 | const params: Record = { page: String(page) }
11 | if (withDeleted) params.withDeleted = '1'
12 | const qs = new URLSearchParams(params).toString()
13 |
14 | const res = await makeValidatedGetRequest(`${url}?${qs}`, z.object({
15 | entries: z.array(leaderboardEntrySchema),
16 | count: z.number(),
17 | itemsPerPage: z.number()
18 | }))
19 |
20 | return res
21 | }
22 |
23 | const { data, error, mutate } = useSWR(
24 | leaderboardId ? [`/games/${activeGame.id}/leaderboards/${leaderboardId}/entries`, page, withDeleted] : null,
25 | fetcher
26 | )
27 |
28 | return {
29 | entries: data?.entries ?? [],
30 | count: data?.count,
31 | itemsPerPage: data?.itemsPerPage,
32 | loading: !data && !error,
33 | error: error && buildError(error),
34 | mutate
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/api/useLeaderboards.ts:
--------------------------------------------------------------------------------
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 { leaderboardSchema } from '../entities/leaderboard'
7 |
8 | export default function useLeaderboards(activeGame: Game) {
9 | const fetcher = async ([url]: [string]) => {
10 | const res = await makeValidatedGetRequest(url, z.object({
11 | leaderboards: z.array(leaderboardSchema)
12 | }))
13 |
14 | return res
15 | }
16 |
17 | const { data, error, mutate } = useSWR(
18 | [`/games/${activeGame.id}/leaderboards`],
19 | fetcher
20 | )
21 |
22 | return {
23 | leaderboards: data?.leaderboards ?? [],
24 | loading: !data && !error,
25 | error: error && buildError(error),
26 | mutate
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/api/useOrganisation.ts:
--------------------------------------------------------------------------------
1 | import useSWR from 'swr'
2 | import buildError from '../utils/buildError'
3 | import makeValidatedGetRequest from './makeValidatedGetRequest'
4 | import { z } from 'zod'
5 | import { gameSchema } from '../entities/game'
6 | import { userSchema } from '../entities/user'
7 | import { inviteSchema } from '../entities/invite'
8 |
9 | export const currentOrganisationSchema = z.object({
10 | games: z.array(gameSchema),
11 | members: z.array(userSchema),
12 | pendingInvites: z.array(inviteSchema)
13 | })
14 |
15 | export default function useOrganisation() {
16 | const fetcher = async ([url]: [string]) => {
17 | const res = await makeValidatedGetRequest(url, currentOrganisationSchema)
18 |
19 | return res
20 | }
21 |
22 | const { data, error, mutate } = useSWR(
23 | ['/organisations/current'],
24 | fetcher
25 | )
26 |
27 | return {
28 | games: data?.games ?? [],
29 | members: data?.members ?? [],
30 | pendingInvites: data?.pendingInvites ?? [],
31 | loading: !data && !error,
32 | error: error && buildError(error),
33 | mutate
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/api/useOrganisationPricingPlan.ts:
--------------------------------------------------------------------------------
1 | import useSWR from 'swr'
2 | import buildError from '../utils/buildError'
3 | import makeValidatedGetRequest from './makeValidatedGetRequest'
4 | import { z } from 'zod'
5 | import { organisationPricingPlanSchema } from '../entities/organisation'
6 |
7 | export default function useOrganisationPricingPlan() {
8 | const fetcher = async ([url]: [string]) => {
9 | const res = await makeValidatedGetRequest(url, z.object({
10 | pricingPlan: organisationPricingPlanSchema
11 | }))
12 |
13 | return res
14 | }
15 |
16 | const { data, error } = useSWR(
17 | ['/billing/organisation-plan'],
18 | fetcher
19 | )
20 |
21 | return {
22 | plan: data?.pricingPlan,
23 | loading: !data && !error,
24 | error: error && buildError(error)
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/api/usePinnedGroups.ts:
--------------------------------------------------------------------------------
1 | import useSWR from 'swr'
2 | import buildError from '../utils/buildError'
3 | import { Game } from '../entities/game'
4 | import makeValidatedGetRequest from './makeValidatedGetRequest'
5 | import { playerGroupSchema } from '../entities/playerGroup'
6 | import { z } from 'zod'
7 |
8 | export default function usePinnedGroups(activeGame: Game | null, includeDevData?: boolean) {
9 | const fetcher = async ([url]: [string]) => {
10 | const res = await makeValidatedGetRequest(url, z.object({
11 | groups: z.array(playerGroupSchema)
12 | }))
13 |
14 | return res
15 | }
16 |
17 | const { data, error, mutate } = useSWR(
18 | activeGame ? [`games/${activeGame.id}/player-groups/pinned`, includeDevData] : null,
19 | fetcher
20 | )
21 |
22 | return {
23 | groups: data?.groups ?? [],
24 | loading: !data && !error,
25 | error: error && buildError(error),
26 | mutate
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/api/usePlayerAuthActivities.ts:
--------------------------------------------------------------------------------
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 { playerAuthActivitySchema } from '../entities/playerAuthActivity'
7 | import canPerformAction, { PermissionBasedAction } from '../utils/canPerformAction'
8 | import { AuthedUser } from '../state/userState'
9 |
10 | export default function usePlayerAuthActivities(activeGame: Game, playerId: string, user: AuthedUser) {
11 | const fetcher = async ([url]: [string]) => {
12 | const res = await makeValidatedGetRequest(url, z.object({
13 | activities: z.array(playerAuthActivitySchema)
14 | }))
15 |
16 | return res
17 | }
18 |
19 | const { data, error } = useSWR(
20 | canPerformAction(user, PermissionBasedAction.VIEW_PLAYER_AUTH_ACTIVITIES) && playerId
21 | ? [`/games/${activeGame.id}/players/${playerId}/auth-activities`]
22 | : null,
23 | fetcher
24 | )
25 |
26 | return {
27 | activities: data?.activities ?? [],
28 | loading: !data && !error,
29 | error: error && buildError(error)
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/api/usePlayerEvents.ts:
--------------------------------------------------------------------------------
1 | import useSWR from 'swr'
2 | import buildError from '../utils/buildError'
3 | import { Game } from '../entities/game'
4 | import makeValidatedGetRequest from './makeValidatedGetRequest'
5 | import { eventSchema } from '../entities/event'
6 | import { z } from 'zod'
7 |
8 | export default function usePlayerEvents(activeGame: Game, playerId: string, search: string, page: number) {
9 | const fetcher = async ([url]: [string]) => {
10 | const qs = new URLSearchParams({ search, page: String(page) }).toString()
11 |
12 | const res = await makeValidatedGetRequest(`${url}?${qs}`, z.object({
13 | events: z.array(eventSchema),
14 | count: z.number(),
15 | itemsPerPage: z.number()
16 | }))
17 |
18 | return res
19 | }
20 |
21 | const { data, error } = useSWR(
22 | [`/games/${activeGame.id}/players/${playerId}/events`, search, page],
23 | fetcher
24 | )
25 |
26 | return {
27 | events: data?.events ?? [],
28 | count: data?.count,
29 | itemsPerPage: data?.itemsPerPage,
30 | loading: !data && !error,
31 | error: error && buildError(error),
32 | errorStatusCode: error && error.response?.status
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/api/usePlayerHeadlines.ts:
--------------------------------------------------------------------------------
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 { PlayerHeadlines } from '../entities/playerHeadline'
7 |
8 | const defaultHeadlines: PlayerHeadlines = {
9 | total_players: { count: 0 },
10 | online_players: { count: 0 }
11 | }
12 |
13 | export default function usePlayerHeadlines(activeGame: Game | null, includeDevData: boolean) {
14 | const fetcher = async ([url]: [string]) => {
15 | const headlines: (keyof PlayerHeadlines)[] = ['total_players', 'online_players']
16 | const res = await Promise.all(headlines.map((headline) => makeValidatedGetRequest(`${url}/${headline}`, z.object({
17 | count: z.number()
18 | }))))
19 |
20 | return headlines.reduce((acc, curr, idx) => ({
21 | ...acc,
22 | [curr]: res[idx]
23 | }), defaultHeadlines)
24 | }
25 |
26 | const { data, error } = useSWR(
27 | activeGame ? [`/games/${activeGame.id}/headlines`, includeDevData] : null,
28 | fetcher
29 | )
30 |
31 | return {
32 | headlines: data ?? defaultHeadlines,
33 | loading: !data && !error,
34 | error: error && buildError(error)
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/api/usePlayerLeaderboardEntries.ts:
--------------------------------------------------------------------------------
1 | import useSWR from 'swr'
2 | import buildError from '../utils/buildError'
3 | import { Game } from '../entities/game'
4 | import { Player } from '../entities/player'
5 | import { PlayerAlias } from '../entities/playerAlias'
6 | import { Leaderboard } from '../entities/leaderboard'
7 | import makeValidatedGetRequest from './makeValidatedGetRequest'
8 | import { z } from 'zod'
9 | import { leaderboardEntrySchema } from '../entities/leaderboardEntry'
10 |
11 | export default function usePlayerLeaderboardEntries(activeGame: Game, leaderboards: Leaderboard[], player: Player | null) {
12 | const fetcher = async ([activeGame, leaderboards, aliases]: [Game, Leaderboard[], PlayerAlias[]]) => {
13 | const urls = aliases.flatMap((alias) => {
14 | return leaderboards.map((leaderboard) => `/games/${activeGame.id}/leaderboards/${leaderboard.id}/entries?aliasId=${alias.id}&page=0`)
15 | })
16 |
17 | const res = await Promise.all(urls.map((url) => makeValidatedGetRequest(url, z.object({
18 | entries: z.array(leaderboardEntrySchema)
19 | }))))
20 |
21 | return {
22 | entries: res.flatMap((res) => res.entries)
23 | }
24 | }
25 |
26 | const { data, error, mutate } = useSWR(
27 | leaderboards && player ? [activeGame, leaderboards, player.aliases] : null,
28 | fetcher
29 | )
30 |
31 | return {
32 | entries: data?.entries ?? [],
33 | loading: !data && !error,
34 | error: error && buildError(error),
35 | mutate
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/api/usePlayerSaves.ts:
--------------------------------------------------------------------------------
1 | import useSWR from 'swr'
2 | import buildError from '../utils/buildError'
3 | import { Game } from '../entities/game'
4 | import makeValidatedGetRequest from './makeValidatedGetRequest'
5 | import { gameSaveSchema } from '../entities/gameSave'
6 | import { z } from 'zod'
7 |
8 | export default function usePlayerSaves(activeGame: Game, playerId: string) {
9 | const fetcher = async ([url]: [string]) => {
10 | const res = await makeValidatedGetRequest(url, z.object({
11 | saves: z.array(gameSaveSchema)
12 | }))
13 |
14 | return res
15 | }
16 |
17 | const { data, error } = useSWR(
18 | [`/games/${activeGame.id}/players/${playerId}/saves`],
19 | fetcher
20 | )
21 |
22 | return {
23 | saves: data?.saves ?? [],
24 | loading: !data && !error,
25 | error: error && buildError(error),
26 | errorStatusCode: error && error.response?.status
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/api/usePlayerStats.ts:
--------------------------------------------------------------------------------
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 { playerGameStatSchema } from '../entities/playerGameStat'
7 |
8 | const usePlayerStats = (activeGame: Game, playerId: string) => {
9 | const fetcher = async ([url]: [string]) => {
10 | const res = await makeValidatedGetRequest(url, z.object({
11 | stats: z.array(playerGameStatSchema)
12 | }))
13 |
14 | return res
15 | }
16 |
17 | const { data, error, mutate } = useSWR(
18 | [`/games/${activeGame.id}/players/${playerId}/stats`],
19 | fetcher
20 | )
21 |
22 | return {
23 | stats: data?.stats ?? [],
24 | loading: !data && !error,
25 | error: error && buildError(error),
26 | errorStatusCode: error && error.response?.status,
27 | mutate
28 | }
29 | }
30 |
31 | export default usePlayerStats
32 |
--------------------------------------------------------------------------------
/src/api/usePlayers.ts:
--------------------------------------------------------------------------------
1 | import useSWR from 'swr'
2 | import buildError from '../utils/buildError'
3 | import { Game } from '../entities/game'
4 | import { z } from 'zod'
5 | import { playerSchema } from '../entities/player'
6 | import makeValidatedGetRequest from './makeValidatedGetRequest'
7 |
8 | export default function usePlayers(activeGame: Game, search: string, page: number) {
9 | const fetcher = async ([url, search, page]: [string, string, number]) => {
10 | const qs = new URLSearchParams({
11 | search,
12 | page: String(page)
13 | }).toString()
14 |
15 | const res = await makeValidatedGetRequest(`${url}?${qs}`, z.object({
16 | players: z.array(playerSchema),
17 | count: z.number(),
18 | itemsPerPage: z.number()
19 | }))
20 |
21 | return res
22 | }
23 |
24 | const { data, error } = useSWR(
25 | [`games/${activeGame.id}/players`, search, page],
26 | fetcher
27 | )
28 |
29 | return {
30 | players: data?.players ?? [],
31 | count: data?.count,
32 | itemsPerPage: data?.itemsPerPage,
33 | loading: !data && !error,
34 | error: error && buildError(error)
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/api/usePricingPlanUsage.ts:
--------------------------------------------------------------------------------
1 | import useSWR from 'swr'
2 | import buildError from '../utils/buildError'
3 | import makeValidatedGetRequest from './makeValidatedGetRequest'
4 | import { z } from 'zod'
5 | import { PricingPlanUsage } from '../entities/pricingPlan'
6 |
7 | export default function usePricingPlanUsage(run: boolean = true) {
8 | const fetcher = async ([url]: [string]) => {
9 | const res = await makeValidatedGetRequest(url, z.object({
10 | usage: z.object({
11 | limit: z.number().nullable(),
12 | used: z.number()
13 | })
14 | }))
15 |
16 | return res
17 | }
18 |
19 | const { data, error } = useSWR(
20 | run ? ['/billing/usage'] : null,
21 | fetcher
22 | )
23 |
24 | const limit = data?.usage.limit ?? (typeof data === 'undefined' ? 0 : Infinity)
25 | const used = data?.usage.used ?? 0
26 |
27 | return {
28 | usage: { limit, used } satisfies PricingPlanUsage,
29 | loading: !data && !error,
30 | error: error && buildError(error)
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/api/usePricingPlans.ts:
--------------------------------------------------------------------------------
1 | import useSWR from 'swr'
2 | import buildError from '../utils/buildError'
3 | import makeValidatedGetRequest from './makeValidatedGetRequest'
4 | import { z } from 'zod'
5 | import { pricingPlanProductSchema } from '../entities/pricingPlan'
6 |
7 | export default function usePricingPlans() {
8 | const fetcher = async ([url]: [string]) => {
9 | const res = await makeValidatedGetRequest(url, z.object({
10 | pricingPlans: z.array(pricingPlanProductSchema)
11 | }))
12 |
13 | return res
14 | }
15 |
16 | const { data, error } = useSWR(
17 | ['/billing/plans'],
18 | fetcher
19 | )
20 |
21 | return {
22 | plans: data?.pricingPlans ?? [],
23 | loading: !data && !error,
24 | error: error && buildError(error)
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/api/useStats.ts:
--------------------------------------------------------------------------------
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 { gameStatSchema } from '../entities/gameStat'
7 |
8 | export default function useStats(activeGame: Game | null, includeDevData?: boolean) {
9 | const fetcher = async ([url]: [string]) => {
10 | const res = await makeValidatedGetRequest(url, z.object({
11 | stats: z.array(gameStatSchema)
12 | }))
13 |
14 | return res
15 | }
16 |
17 | const { data, error, mutate } = useSWR(
18 | activeGame ? [`/games/${activeGame.id}/game-stats`, includeDevData] : null,
19 | fetcher
20 | )
21 |
22 | return {
23 | stats: data?.stats ?? [],
24 | loading: !data && !error,
25 | error: error && buildError(error),
26 | mutate
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/api/verify2FA.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod'
2 | import api from './api'
3 | import makeValidatedRequest from './makeValidatedRequest'
4 | import { userSchema } from '../entities/user'
5 |
6 | const verify2FA = makeValidatedRequest(
7 | (code: string, userId: number) => api.post('/public/users/2fa', { code, userId }),
8 | z.object({
9 | accessToken: z.string(),
10 | user: userSchema
11 | })
12 | )
13 |
14 | export default verify2FA
15 |
--------------------------------------------------------------------------------
/src/api/viewRecoveryCodes.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod'
2 | import api from './api'
3 | import makeValidatedRequest from './makeValidatedRequest'
4 |
5 | const viewRecoveryCodes = makeValidatedRequest(
6 | (password: string) => api.post('/users/2fa/recovery_codes/view', { password }),
7 | z.object({
8 | recoveryCodes: z.array(z.string())
9 | })
10 | )
11 |
12 | export default viewRecoveryCodes
13 |
--------------------------------------------------------------------------------
/src/assets/talo-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/assets/talo-service.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/components/ActivityRenderer.tsx:
--------------------------------------------------------------------------------
1 | import SecondaryTitle from './SecondaryTitle'
2 | import { format } from 'date-fns'
3 |
4 | type ActivityRendererProps = {
5 | section: {
6 | date: Date
7 | items: {
8 | createdAt: string
9 | description: string
10 | extra?: Record
11 | }[]
12 | }
13 | }
14 |
15 | export default function ActivityRenderer({ section }: ActivityRendererProps) {
16 | return (
17 |
18 |
{format(section.date, 'dd MMM Y')}
19 |
20 | {section.items.map((item, itemIdx) => (
21 |
22 |
{format(new Date(item.createdAt), 'HH:mm')} {item.description}
23 |
24 | {item.extra &&
25 |
26 | {Object.keys(item.extra).sort((a, b) => {
27 | if (b === 'Player') return 1
28 |
29 | return a.localeCompare(b)
30 | }).map((key) => (
31 | {key} = {String((item.extra ?? {})[key])}
32 | ))}
33 |
34 | }
35 |
36 | ))}
37 |
38 | )
39 | }
40 |
--------------------------------------------------------------------------------
/src/components/AlertBanner.tsx:
--------------------------------------------------------------------------------
1 | import type { Icon, IconProps } from '@tabler/icons-react'
2 | import { IconAlertCircle } from '@tabler/icons-react'
3 | import type { ForwardRefExoticComponent, ReactNode, RefAttributes } from 'react'
4 | import clsx from 'clsx'
5 |
6 | type AlertBannerIcon = ForwardRefExoticComponent & RefAttributes>
7 |
8 | type AlertBannerProps = {
9 | text: ReactNode | string
10 | className?: string
11 | icon?: AlertBannerIcon
12 | }
13 |
14 | export default function AlertBanner({
15 | icon: Icon = IconAlertCircle,
16 | text,
17 | className
18 | }: AlertBannerProps) {
19 | return (
20 |
21 |
22 | {text}
23 |
24 | )
25 | }
26 |
--------------------------------------------------------------------------------
/src/components/Checkbox.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx'
2 | import { IconCheck } from '@tabler/icons-react'
3 | import { focusStyle } from '../styles/theme'
4 | import type { ReactNode, Ref } from 'react'
5 |
6 | type CheckboxProps = {
7 | id: string
8 | checked: boolean | undefined
9 | onChange: (checked: boolean) => void
10 | labelContent: ReactNode
11 | inputRef?: Ref
12 | }
13 |
14 | export default function Checkbox({
15 | id,
16 | checked,
17 | onChange,
18 | labelContent,
19 | inputRef
20 | }: CheckboxProps) {
21 | return (
22 |
23 |
24 | onChange(e.target.checked)}
31 | />
32 |
33 |
34 |
35 |
36 | {labelContent}
37 |
38 | )
39 | }
40 |
--------------------------------------------------------------------------------
/src/components/CheckboxButton.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 | import clsx from 'clsx'
3 | import { IconCheck } from '@tabler/icons-react'
4 | import { hiddenInputStyle, labelFocusStyle } from '../styles/theme'
5 |
6 | type CheckboxButtonProps = {
7 | id: string
8 | checked: boolean
9 | onChange: (checked: boolean) => void
10 | label: string
11 | }
12 |
13 | export default function CheckboxButton({ id, checked, label, onChange }: CheckboxButtonProps) {
14 | const [focus, setFocus] = useState(false)
15 |
16 | return (
17 | <>
18 | setFocus(true)}
23 | onBlur={() => setFocus(false)}
24 | onChange={() => onChange(!checked)}
25 | />
26 |
27 |
31 |
32 | {checked &&
33 |
34 |
35 |
36 | }
37 |
38 | {label}
39 |
40 | >
41 | )
42 | }
43 |
--------------------------------------------------------------------------------
/src/components/DevDataStatus.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx'
2 | import { useRecoilValue } from 'recoil'
3 | import devDataState from '../state/devDataState'
4 | import SecondaryTitle from './SecondaryTitle'
5 | import DevDataToggle from './toggles/DevDataToggle'
6 |
7 | function DevDataStatus() {
8 | const includeDevData = useRecoilValue(devDataState)
9 |
10 | return (
11 |
12 |
Dev data is currently
13 |
14 | {' '}{includeDevData ? 'enabled' : 'not enabled'}
15 |
16 |
17 |
18 |
When enabled, you'll see data submitted by players from dev builds of your game
19 |
20 |
21 |
22 | )
23 | }
24 |
25 | export default DevDataStatus
26 |
--------------------------------------------------------------------------------
/src/components/ErrorMessage.tsx:
--------------------------------------------------------------------------------
1 | import { IconAlertCircle } from '@tabler/icons-react'
2 | import clsx from 'clsx'
3 | import type { ReactNode } from 'react'
4 |
5 | export type TaloError = {
6 | message: string
7 | keys?: { [key: string]: string[] }
8 | hasKeys?: boolean
9 | extra?: { [key: string]: string[] }
10 | }
11 |
12 | type ErrorMessageProps = {
13 | error: TaloError | null
14 | children?: ReactNode
15 | className?: string
16 | }
17 |
18 | function ErrorMessage({
19 | error,
20 | children,
21 | className
22 | }: ErrorMessageProps) {
23 | const hasMultipleErrors = error?.hasKeys && Object.keys(error.keys!).length > 1
24 |
25 | return (
26 |
27 |
28 |
29 |
30 | {error?.hasKeys &&
31 | Object.entries(error.keys!).map(([key, value]) => (
32 |
33 | {key}: {value.join(', ')}
34 |
35 | ))
36 | }
37 |
38 | {!error?.hasKeys && error?.message &&
39 |
{error.message}
40 | }
41 |
42 | {children}
43 |
44 |
45 |
46 | )
47 | }
48 |
49 | export default ErrorMessage
50 |
--------------------------------------------------------------------------------
/src/components/HeadlineStat.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx'
2 |
3 | type HeadlineStatProps = {
4 | title: string
5 | stat: string | number
6 | className?: string
7 | }
8 |
9 | export default function HeadlineStat({ className, title, stat }: HeadlineStatProps) {
10 | return (
11 |
12 |
13 |
{title}
14 |
15 |
16 |
17 |
{stat.toLocaleString()}
18 |
19 |
20 | )
21 | }
22 |
--------------------------------------------------------------------------------
/src/components/Identifier.tsx:
--------------------------------------------------------------------------------
1 | import type { ReactNode } from 'react'
2 |
3 | type IdentifierProps = {
4 | id: string
5 | children?: ReactNode
6 | }
7 |
8 | export default function Identifier({
9 | id,
10 | children
11 | }: IdentifierProps) {
12 | return (
13 |
14 |
15 | {children}
16 | {id}
17 |
18 |
19 | )
20 | }
21 |
--------------------------------------------------------------------------------
/src/components/IntendedRouteHandler.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react'
2 | import { Navigate } from 'react-router-dom'
3 | import routes from '../constants/routes'
4 |
5 | type IntendedRouteHandlerProps = {
6 | intendedRoute: string | null
7 | }
8 |
9 | // 1. user hits dashboard.trytalo.com/leaderboards but is logged out
10 | // 2. user is redirected to dashboard.trytalo.com/?next=%2Fleaderboards
11 | // 3. "next" search param is put into sessionStorage
12 | // 4. useIntendedRoute hook picks up logic to redirect based on the sessionStorage
13 | export default function IntendedRouteHandler({
14 | intendedRoute
15 | }: IntendedRouteHandlerProps) {
16 | useEffect(() => {
17 | const next = new URLSearchParams(window.location.search).get('next')
18 | if (next) {
19 | window.sessionStorage.setItem('intendedRoute', next)
20 | }
21 | }, [])
22 |
23 | if (!intendedRoute) {
24 |
25 | }
26 |
27 | return (
28 |
29 | )
30 | }
31 |
--------------------------------------------------------------------------------
/src/components/Link.tsx:
--------------------------------------------------------------------------------
1 | import { Link as RouterLink } from 'react-router-dom'
2 | import { focusStyle, linkStyle } from '../styles/theme'
3 | import clsx from 'clsx'
4 | import type { ReactNode } from 'react'
5 |
6 | type LinkProps = {
7 | to: string
8 | state?: object
9 | className?: string
10 | children: ReactNode
11 | }
12 |
13 | export default function Link({
14 | to,
15 | state,
16 | className,
17 | children
18 | }: LinkProps) {
19 | const linkClass = clsx(linkStyle, focusStyle, className ?? '')
20 |
21 | if (to.startsWith('http')) {
22 | return (
23 | {children}
24 | )
25 | }
26 |
27 | return (
28 |
33 | {children}
34 |
35 | )
36 | }
37 |
--------------------------------------------------------------------------------
/src/components/LinkButton.tsx:
--------------------------------------------------------------------------------
1 | import { MouseEvent, ReactNode } from 'react'
2 | import { linkStyle, focusStyle } from '../styles/theme'
3 | import clsx from 'clsx'
4 |
5 | type LinkButtonProps = {
6 | onClick: (e: MouseEvent) => void
7 | className?: string
8 | children: ReactNode
9 | }
10 |
11 | export default function LinkButton({ onClick, className, children }: LinkButtonProps) {
12 | return (
13 |
18 | {children}
19 |
20 | )
21 | }
22 |
--------------------------------------------------------------------------------
/src/components/Loading.tsx:
--------------------------------------------------------------------------------
1 | // Spinner from unmaintained package: https://github.com/adexin/spinners-react
2 | //
3 | import colors from 'tailwindcss/colors'
4 |
5 | type LoadingProps = {
6 | colour?: string
7 | size?: number
8 | thickness?: number
9 | }
10 |
11 | export default function Loading({
12 | size = 80,
13 | thickness = 160
14 | }: LoadingProps) {
15 | const speed = 100
16 | const strokeWidth = 4 * (thickness / 100)
17 |
18 | return (
19 |
20 |
21 |
29 |
41 |
42 |
43 | )
44 | }
45 |
--------------------------------------------------------------------------------
/src/components/MobileMenu.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react'
2 | import clsx from 'clsx'
3 | import { IconArrowLeft } from '@tabler/icons-react'
4 | import Button from './Button'
5 | import { useLocation } from 'react-router-dom'
6 | import type { ReactNode } from 'react'
7 |
8 | type MobileMenuProps = {
9 | children: ReactNode
10 | visible: boolean
11 | onClose: () => void
12 | }
13 |
14 | export default function MobileMenu({ children, visible, onClose }: MobileMenuProps) {
15 | const location = useLocation()
16 | const [pathname, setPathname] = useState(location.pathname)
17 |
18 | useEffect(() => {
19 | setPathname(location.pathname)
20 | }, [visible, location])
21 |
22 | useEffect(() => {
23 | if (visible && location.pathname !== pathname) {
24 | onClose()
25 | }
26 | }, [location.pathname, pathname, visible, onClose])
27 |
28 | return (
29 |
36 |
41 |
42 |
43 |
44 |
45 |
46 |
49 |
50 | )
51 | }
52 |
--------------------------------------------------------------------------------
/src/components/Page.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx'
2 | import Title from './Title'
3 | import Loading from './Loading'
4 | import GlobalBanners from './GlobalBanners'
5 | import type { ReactNode } from 'react'
6 |
7 | type PageProps = {
8 | title: string
9 | showBackButton?: boolean
10 | isLoading?: boolean
11 | containerClassName?: string
12 | children: ReactNode
13 | extraTitleComponent?: ReactNode
14 | secondaryNav?: ReactNode
15 | disableBanners?: boolean
16 | }
17 |
18 | export default function Page({
19 | title,
20 | showBackButton = false,
21 | isLoading = false,
22 | containerClassName = '',
23 | extraTitleComponent,
24 | children,
25 | secondaryNav,
26 | disableBanners
27 | }: PageProps) {
28 | return (
29 |
30 | {secondaryNav}
31 |
32 | {!disableBanners &&
}
33 |
34 |
35 |
36 |
{title}
37 |
38 | {isLoading &&
39 |
40 |
41 |
42 | }
43 |
44 | {!isLoading && extraTitleComponent}
45 |
46 |
47 | {children}
48 |
49 |
50 | )
51 | }
52 |
--------------------------------------------------------------------------------
/src/components/Pagination.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx'
2 | import Button from './Button'
3 | import { Dispatch, SetStateAction } from 'react'
4 |
5 | type PaginationProps = {
6 | count: number
7 | pageState: [number, Dispatch>]
8 | itemsPerPage: number
9 | }
10 |
11 | export default function Pagination({
12 | count,
13 | pageState,
14 | itemsPerPage
15 | }: PaginationProps) {
16 | const totalPages = Math.ceil(count / itemsPerPage)
17 |
18 | if (totalPages === 1) return null
19 |
20 | const [page, setPage] = pageState
21 | const pages = [...new Array(totalPages)].map((_, idx) => String(idx + 1))
22 |
23 | return (
24 |
25 |
26 | {pages.map((val, idx) => (
27 |
28 | setPage(idx)}
37 | >
38 | {val}
39 |
40 |
41 | ))}
42 |
43 |
44 | )
45 | }
46 |
--------------------------------------------------------------------------------
/src/components/PlayerIdentifier.tsx:
--------------------------------------------------------------------------------
1 | import Identifier from './Identifier'
2 |
3 | type PlayerIdentifierProps = {
4 | player?: {
5 | id: string
6 | devBuild: boolean
7 | }
8 | }
9 |
10 | function PlayerIdentifier({
11 | player
12 | }: PlayerIdentifierProps) {
13 | return (
14 |
15 | {player?.devBuild && DEV }
16 |
17 | )
18 | }
19 |
20 | export default PlayerIdentifier
21 |
--------------------------------------------------------------------------------
/src/components/SecondaryNav.tsx:
--------------------------------------------------------------------------------
1 | import Link from './Link'
2 | import clsx from 'clsx'
3 | import { useLocation } from 'react-router-dom'
4 | import canViewPage from '../utils/canViewPage'
5 | import userState from '../state/userState'
6 | import { useRecoilValue } from 'recoil'
7 |
8 | type SecondaryNavProps = {
9 | routes: {
10 | title: string
11 | to: string
12 | }[]
13 | }
14 |
15 | function SecondaryNav({
16 | routes
17 | }: SecondaryNavProps) {
18 | const location = useLocation()
19 | const user = useRecoilValue(userState)
20 |
21 | const availableRoutes = routes.filter(({ to }) => canViewPage(user, to))
22 | if (availableRoutes.length < 2) return null
23 |
24 | return (
25 |
26 |
27 | {availableRoutes.map(({ title, to }) => {
28 | const active = location.pathname === to
29 |
30 | return
37 |
41 | {title}
42 |
43 |
44 | })}
45 |
46 |
47 |
48 | )
49 | }
50 |
51 | export default SecondaryNav
52 |
--------------------------------------------------------------------------------
/src/components/SecondaryTitle.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx'
2 | import type { ReactNode } from 'react'
3 |
4 | type SecondaryTitleProps = {
5 | className?: string
6 | children: ReactNode
7 | }
8 |
9 | export default function SecondaryTitle({
10 | className,
11 | children
12 | }: SecondaryTitleProps) {
13 | return {children}
14 | }
15 |
--------------------------------------------------------------------------------
/src/components/TaloInfoCard.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 | import LinkButton from './LinkButton'
3 | import clsx from 'clsx'
4 | import { IconChevronDown } from '@tabler/icons-react'
5 | import Link from './Link'
6 |
7 | export default function TaloInfoCard() {
8 | const [showInfo, setShowInfo] = useState(false)
9 |
10 | return (
11 | <>
12 | setShowInfo(!showInfo)}>
13 | What is Talo?
14 |
15 |
16 |
17 | {showInfo &&
18 |
19 |
20 |
21 | Talo is an open-source game backend: it's the easiest way to integrate leaderboards, stats, player management and more.
22 | {' '} Learn more here.
23 |
24 |
25 | }
26 | >
27 | )
28 | }
29 |
--------------------------------------------------------------------------------
/src/components/Tile.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx'
2 | import type { ReactNode } from 'react'
3 |
4 | type TileProps = {
5 | header: ReactNode
6 | content: ReactNode
7 | footer?: ReactNode
8 | selected?: boolean
9 | }
10 |
11 | export default function Tile({
12 | header,
13 | content,
14 | footer,
15 | selected
16 | }: TileProps) {
17 | return (
18 |
25 |
26 |
{header}
27 |
28 |
{content}
29 |
30 | {footer}
31 |
32 |
33 | )
34 | }
35 |
--------------------------------------------------------------------------------
/src/components/TimePeriodPicker.tsx:
--------------------------------------------------------------------------------
1 | import { TimePeriod } from '../utils/useTimePeriod'
2 | import Button from './Button'
3 | import clsx from 'clsx'
4 |
5 | export type LabelledTimePeriod = {
6 | id: TimePeriod
7 | label: string
8 | }
9 |
10 | type TimePeriodPickerProps = {
11 | periods: LabelledTimePeriod[]
12 | onPick: (period: LabelledTimePeriod) => void
13 | selectedPeriod: TimePeriod | null
14 | }
15 |
16 | export default function TimePeriodPicker({ periods, onPick, selectedPeriod }: TimePeriodPickerProps) {
17 | const buttonClassName = 'text-sm md:text-base border-2 border-l-0 py-1 px-1 md:py-1.5 md:px-2 border-indigo-500 rounded-none first:rounded-l first:border-l-2 last:rounded-r hover:bg-gray-900 ring-inset'
18 |
19 | return (
20 |
21 | {periods.map((period) => (
22 | onPick(period)}
27 | >
28 | {period.label}
29 |
30 | ))}
31 |
32 | )
33 | }
34 |
--------------------------------------------------------------------------------
/src/components/Title.tsx:
--------------------------------------------------------------------------------
1 | import { IconArrowLeft } from '@tabler/icons-react'
2 | import Button from './Button'
3 | import { useNavigate } from 'react-router-dom'
4 | import clsx from 'clsx'
5 | import type { ReactNode } from 'react'
6 |
7 | type TitleProps = {
8 | children: ReactNode
9 | showBackButton?: boolean
10 | className?: string
11 | }
12 |
13 | function Title({
14 | children,
15 | showBackButton,
16 | className
17 | }: TitleProps) {
18 | const navigate = useNavigate()
19 |
20 | return (
21 |
22 | {showBackButton &&
23 | navigate(-1)}
27 | icon={ }
28 | extra={{ 'aria-label': 'Go back' }}
29 | />
30 | }
31 |
32 | {children}
33 |
34 | )
35 | }
36 |
37 | export default Title
38 |
--------------------------------------------------------------------------------
/src/components/UsageWarningBanner.tsx:
--------------------------------------------------------------------------------
1 | import { Link } from 'react-router-dom'
2 | import AlertBanner from './AlertBanner'
3 | import { IconInfoCircle } from '@tabler/icons-react'
4 | import { PricingPlanUsage } from '../entities/pricingPlan'
5 |
6 | type UsageWarningBannerProps = {
7 | usage: PricingPlanUsage
8 | }
9 |
10 | export default function UsageWarningBanner({ usage }: UsageWarningBannerProps) {
11 | return (
12 | You've used {Math.round((usage.used / usage.limit) * 100)}% of your current plan's limit. Upgrade your plan to avoid any disruption to your game.}
16 | />
17 | )
18 | }
19 |
--------------------------------------------------------------------------------
/src/components/__tests__/DateInput.test.tsx:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react'
2 | import userEvent, { PointerEventsCheckLevel } from '@testing-library/user-event'
3 | import DateInput from '../DateInput'
4 |
5 | describe(' ', () => {
6 | beforeAll(() => {
7 | vi.setSystemTime(new Date(2022, 0, 1))
8 | })
9 |
10 | it('should the date as dd MMM yyyy', () => {
11 | render(
12 |
16 | )
17 |
18 | expect(screen.getByDisplayValue('03 Mar 2022')).toBeInTheDocument()
19 | })
20 |
21 | it('should render the default as today', () => {
22 | render(
23 |
27 | )
28 |
29 | expect(screen.getByDisplayValue('01 Jan 2022')).toBeInTheDocument()
30 | })
31 |
32 | it('should pick a date', async () => {
33 | const changeMock = vi.fn()
34 |
35 | render(
36 | changeMock(value)}
39 | value=''
40 | />
41 | )
42 |
43 | await userEvent.click(screen.getByDisplayValue('01 Jan 2022'))
44 | expect(await screen.findByText('January 2022')).toBeInTheDocument()
45 | await userEvent.click(screen.getByText('15'), { pointerEventsCheck: PointerEventsCheckLevel.Never })
46 | expect(changeMock).toHaveBeenCalledWith('2022-01-15T00:00:00.000Z')
47 | })
48 | })
49 |
--------------------------------------------------------------------------------
/src/components/__tests__/DevDataStatus.test.tsx:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react'
2 | import devDataState from '../../state/devDataState'
3 | import DevDataStatus from '../DevDataStatus'
4 | import KitchenSink from '../../utils/KitchenSink'
5 |
6 | describe(' ', () => {
7 | it('should render the not enabled state', () => {
8 | render(
9 |
10 |
11 |
12 | )
13 |
14 | expect(screen.getByText('not enabled')).toBeInTheDocument()
15 | })
16 |
17 | it('should render the enabled state', () => {
18 | render(
19 |
20 |
21 |
22 | )
23 |
24 | expect(screen.queryByText('not enabled')).not.toBeInTheDocument()
25 | expect(screen.getByText('enabled')).toBeInTheDocument()
26 | })
27 | })
28 |
--------------------------------------------------------------------------------
/src/components/__tests__/ErrorMessage.test.tsx:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react'
2 | import ErrorMessage from '../ErrorMessage'
3 | import buildError from '../../utils/buildError'
4 |
5 | describe(' ', () => {
6 | it('should render all keys in the errors object', () => {
7 | render(
8 |
20 | )
21 |
22 | expect(screen.getByText('startDate: The startDate is invalid')).toBeInTheDocument()
23 | expect(screen.getByText('endDate: The endDate is invalid')).toBeInTheDocument()
24 | })
25 |
26 | it('should just render the message when there are no errors', () => {
27 | render(
28 |
37 | )
38 |
39 | expect(screen.getByText('Something went wrong')).toBeInTheDocument()
40 | })
41 | })
42 |
--------------------------------------------------------------------------------
/src/components/__tests__/Link.test.tsx:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react'
2 | import routes from '../../constants/routes'
3 | import KitchenSink from '../../utils/KitchenSink'
4 | import Link from '../Link'
5 |
6 | describe(' ', () => {
7 | it('should render an internal router link', () => {
8 | render(
9 |
10 | Go to Account
11 |
12 | )
13 |
14 | expect(screen.getByRole('link')).toBeInTheDocument()
15 | })
16 |
17 | it('should render an external link', () => {
18 | render(
19 |
20 | See the docs
21 |
22 | )
23 |
24 | const link = screen.getByRole('link')
25 | expect(link).toBeInTheDocument()
26 | expect(link).toHaveAttribute('rel', 'noreferrer')
27 | })
28 | })
29 |
--------------------------------------------------------------------------------
/src/components/__tests__/MobileMenu.test.tsx:
--------------------------------------------------------------------------------
1 | import { render, screen, waitFor } from '@testing-library/react'
2 | import { BrowserRouter } from 'react-router-dom'
3 | import userEvent from '@testing-library/user-event'
4 | import MobileMenu from '../MobileMenu'
5 | import Link from '../Link'
6 |
7 | describe(' ', () => {
8 | it('should close when going to a different page', async () => {
9 | const closeMock = vi.fn()
10 |
11 | render(
12 |
13 |
14 | Events
15 |
16 |
17 | , { wrapper: BrowserRouter }
18 | )
19 |
20 | await userEvent.click(screen.getByText('Events'))
21 |
22 | await waitFor(() => expect(closeMock).toHaveBeenCalled())
23 | })
24 | })
25 |
--------------------------------------------------------------------------------
/src/components/__tests__/Modal.test.tsx:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react'
2 | import userEvent from '@testing-library/user-event'
3 | import Modal from '../Modal'
4 |
5 | describe(' ', () => {
6 | it('should close when pressing escape', async () => {
7 | const closeMock = vi.fn()
8 |
9 | render(
10 |
15 | Content
16 |
17 | )
18 |
19 | await userEvent.keyboard('{Escape}')
20 |
21 | expect(closeMock).toHaveBeenCalled()
22 | })
23 |
24 | it('should close when clicking the close button', async () => {
25 | const closeMock = vi.fn()
26 |
27 | render(
28 |
33 | Content
34 |
35 | )
36 |
37 | await userEvent.click(screen.getByLabelText('Close modal'))
38 |
39 | expect(closeMock).toHaveBeenCalled()
40 | })
41 | })
42 |
--------------------------------------------------------------------------------
/src/components/__tests__/Pagination.test.tsx:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react'
2 | import userEvent from '@testing-library/user-event'
3 | import Pagination from '../Pagination'
4 |
5 | describe(' ', () => {
6 | it('should render the correct amount of pages', () => {
7 | render( )
8 | expect(screen.getAllByRole('listitem')).toHaveLength(2)
9 | })
10 |
11 | it('should go to the correct page on click', async () => {
12 | const changeMock = vi.fn()
13 |
14 | render( )
15 |
16 | await userEvent.click(screen.getByText('4'))
17 |
18 | expect(changeMock).toHaveBeenCalledWith(3)
19 | })
20 |
21 | it('should not render if there are not enough items to paginate', () => {
22 | render( )
23 |
24 | expect(screen.queryByRole('listitem')).not.toBeInTheDocument()
25 | })
26 | })
27 |
--------------------------------------------------------------------------------
/src/components/__tests__/PlayerAliases.test.tsx:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react'
2 | import PlayerAliases from '../PlayerAliases'
3 | import { PlayerAlias, PlayerAliasService } from '../../entities/playerAlias'
4 | import playerAliasMock from '../../__mocks__/playerAliasMock'
5 |
6 | describe(' ', () => {
7 | it('should render an alias', () => {
8 | const aliases: PlayerAlias[] = [
9 | playerAliasMock({ service: PlayerAliasService.STEAM, identifier: 'yxre12' })
10 | ]
11 |
12 | render( )
13 |
14 | expect(screen.getByText(aliases[0].identifier)).toBeInTheDocument()
15 | })
16 |
17 | it('should render the latest alias and an indicator for how many more', () => {
18 | const aliases: PlayerAlias[] = [
19 | playerAliasMock({ service: PlayerAliasService.STEAM, identifier: 'yxre12', lastSeenAt: '2024-10-28 10:00:00' }),
20 | playerAliasMock({ service: PlayerAliasService.USERNAME, identifier: 'ryet12', lastSeenAt: '2024-10-27 10:00:00' }),
21 | playerAliasMock({ service: PlayerAliasService.EPIC, identifier: 'epic_23rd', lastSeenAt: '2024-10-26 10:00:00' })
22 | ]
23 |
24 | render( )
25 |
26 | expect(screen.getByText(aliases[0].identifier)).toBeInTheDocument()
27 | expect(screen.getByText('+ 2 more')).toBeInTheDocument()
28 | })
29 |
30 | it('should render none if the player has none', () => {
31 | const aliases: PlayerAlias[] = []
32 |
33 | render( )
34 |
35 | expect(screen.getByText('None')).toBeInTheDocument()
36 | })
37 | })
38 |
--------------------------------------------------------------------------------
/src/components/__tests__/PlayerIdentifier.test.tsx:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react'
2 | import PlayerIdentifier from '../PlayerIdentifier'
3 |
4 | describe(' ', () => {
5 | it('should render player identifiers for players not from dev build', () => {
6 | render( )
7 |
8 | expect(screen.getByText('Player = 2a5adaba-fad2-471e-8ec1-47b9b24b5d3a')).toBeInTheDocument()
9 | expect(screen.queryByText('DEV')).not.toBeInTheDocument()
10 | })
11 |
12 | it('should render player identifiers for players from dev build', () => {
13 | render( )
14 |
15 | expect(screen.getByText('Player = f32be3f4-531c-4d28-97ab-1f7dc465cc65')).toBeInTheDocument()
16 | expect(screen.getByText('DEV')).toBeInTheDocument()
17 | })
18 | })
19 |
--------------------------------------------------------------------------------
/src/components/__tests__/SecondaryNav.test.tsx:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react'
2 | import userState from '../../state/userState'
3 | import SecondaryNav from '../SecondaryNav'
4 | import routes from '../../constants/routes'
5 | import KitchenSink from '../../utils/KitchenSink'
6 | import { UserType } from '../../entities/user'
7 |
8 | describe(' ', () => {
9 | it('should filter out pages that the user cannot see', () => {
10 | render(
11 |
12 |
20 |
21 | )
22 |
23 | expect(screen.queryByText('Organisation')).not.toBeInTheDocument()
24 | })
25 |
26 | it('should not render if there are less than 2 routes', () => {
27 | render(
28 |
29 |
34 |
35 | )
36 |
37 | expect(screen.queryByText('Dashboard')).not.toBeInTheDocument()
38 | })
39 | })
40 |
--------------------------------------------------------------------------------
/src/components/__tests__/ServicesLink.test.tsx:
--------------------------------------------------------------------------------
1 | import { render, screen, waitFor } from '@testing-library/react'
2 | import userEvent from '@testing-library/user-event'
3 | import ServicesLink from '../ServicesLink'
4 | import userState from '../../state/userState'
5 | import KitchenSink from '../../utils/KitchenSink'
6 | import routes from '../../constants/routes'
7 | import { UserType } from '../../entities/user'
8 |
9 | describe(' ', () => {
10 | it('should close when going to a different page', async () => {
11 | const setLocationMock = vi.fn()
12 |
13 | render(
14 |
18 |
19 |
20 | )
21 |
22 | await userEvent.click(screen.getByText('Services'))
23 | await userEvent.click(screen.getByText('Players'))
24 |
25 | await waitFor(() => {
26 | expect(setLocationMock).toHaveBeenLastCalledWith({
27 | pathname: routes.players,
28 | state: null
29 | })
30 | })
31 |
32 | await waitFor(() => expect(screen.queryByText('Players')).not.toBeInTheDocument())
33 | })
34 | })
35 |
--------------------------------------------------------------------------------
/src/components/__tests__/TimePeriodPicker.test.tsx:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react'
2 | import userEvent from '@testing-library/user-event'
3 | import TimePeriodPicker, { LabelledTimePeriod } from '../TimePeriodPicker'
4 |
5 | describe(' ', () => {
6 | it('should render time periods', () => {
7 | const periods: LabelledTimePeriod[] = [{ id: '30d', label: '30 days' }, { id: '7d', label: '7 days' }]
8 | render( )
9 |
10 | expect(screen.getAllByRole('button')).toHaveLength(2)
11 |
12 | for (const period of periods) {
13 | expect(screen.getByText(period.label)).toBeInTheDocument()
14 | }
15 | })
16 |
17 | it('should pick the correct time periods', async () => {
18 | const pickMock = vi.fn()
19 | const periods: LabelledTimePeriod[] = [{ id: '30d', label: '30 days' }, { id: '7d', label: '7 days' }]
20 | render( )
21 |
22 | await userEvent.click(screen.getByText(periods[1].label))
23 |
24 | expect(pickMock).toHaveBeenCalledWith(periods[1])
25 | })
26 | })
27 |
--------------------------------------------------------------------------------
/src/components/__tests__/Title.test.tsx:
--------------------------------------------------------------------------------
1 | import { render, screen, waitFor } from '@testing-library/react'
2 | import userEvent from '@testing-library/user-event'
3 | import KitchenSink from '../../utils/KitchenSink'
4 | import Title from '../Title'
5 |
6 | describe(' ', () => {
7 | it('should go back', async () => {
8 | const setLocationMock = vi.fn()
9 |
10 | render(
11 |
12 | Player Props
13 |
14 | )
15 |
16 | expect(screen.getByText('Player Props')).toBeInTheDocument()
17 |
18 | await userEvent.click(screen.getByLabelText('Go back'))
19 |
20 | await waitFor(() => {
21 | expect(setLocationMock).toHaveBeenCalledWith({
22 | pathname: '/players',
23 | state: null
24 | })
25 | })
26 | })
27 | })
28 |
--------------------------------------------------------------------------------
/src/components/billing/BillingPortalTile.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 | import Button from '../Button'
3 | import buildError from '../../utils/buildError'
4 | import createPortalSession from '../../api/createPortalSession'
5 | import { IconExternalLink } from '@tabler/icons-react'
6 | import ErrorMessage, { TaloError } from '../ErrorMessage'
7 | import Tile from '../Tile'
8 |
9 | export default function BillingPortalTile() {
10 | const [portalLoading, setPortalLoading] = useState(false)
11 | const [portalError, setPortalError] = useState(null)
12 |
13 | const onBillingPortalClick = async () => {
14 | setPortalLoading(true)
15 | setPortalError(null)
16 |
17 | try {
18 | const { redirect } = await createPortalSession()
19 | window.location.assign(redirect)
20 | } catch (err) {
21 | setPortalLoading(false)
22 | setPortalError(buildError(err))
23 | }
24 | }
25 |
26 | return (
27 |
28 |
31 | Billing details
32 | }
38 | >
39 | Billing Portal
40 |
41 | >
42 | )}
43 | content={(
44 |
45 |
You can update your billing information and view invoices inside the billing portal
46 |
47 | )}
48 | footer={(
49 | <>
50 | {portalError &&
51 |
52 | }
53 | >
54 | )}
55 | />
56 |
57 | )
58 | }
59 |
--------------------------------------------------------------------------------
/src/components/billing/BillingUsageTile.tsx:
--------------------------------------------------------------------------------
1 | import Tile from '../Tile'
2 | import ErrorMessage, { TaloError } from '../ErrorMessage'
3 | import clsx from 'clsx'
4 | import { PricingPlanUsage } from '../../entities/pricingPlan'
5 |
6 | type BillingUsageTileProps = {
7 | usage: PricingPlanUsage
8 | usageError: TaloError | null
9 | }
10 |
11 | export default function BillingUsageTile({
12 | usage,
13 | usageError
14 | }: BillingUsageTileProps) {
15 | return (
16 |
17 | Usage}
19 | content={
20 | <>
21 | {!usageError &&
22 |