Migrate Next.js to Vite (#1397)

Vite is much simpler to use than Next.js and we don't need any of the
features Next has that Vite doesn't have.

Benefits of moving to Vite are:
- Much better performance in dev and prod environments
- Much better build times
- Actual support for static exports, no vendor lock-in of having to use
Vercel
- Support for runtime environment variables/loading config from `.env`
files
- No annoying backwards-incompatible changes on major releases of Next
- Better i18n support without having to define getServerSideProps on
every page
- Better bundle optimization
- No opt-out Vercel telemetry 

Also replaces yarn by pnpm and upgrades mantine to 8.3
This commit is contained in:
Erik Vroon
2025-11-12 11:18:06 +01:00
committed by GitHub
parent 3f2563b5a2
commit 583eb4e963
104 changed files with 6316 additions and 8189 deletions

View File

@@ -19,17 +19,20 @@ jobs:
steps:
- uses: actions/checkout@v5
- name: Setup yarn
- name: Enable Corepack
run: corepack enable
- name: Setup pnpm
uses: actions/setup-node@v6
with:
node-version: '22'
cache: 'yarn'
cache-dependency-path: frontend/yarn.lock
cache: 'pnpm'
cache-dependency-path: frontend/pnpm-lock.yaml
- name: Install npm modules
run: yarn
run: pnpm i
working-directory: frontend
- name: Run prettier, lint, jest checks
run: yarn test
run: pnpm test
working-directory: frontend

1
.gitignore vendored
View File

@@ -6,6 +6,7 @@ uv.lock
.mypy_cache
.dmypy.json
.pytest_cache
.yarn
*.env
!ci.env

View File

@@ -50,7 +50,7 @@
<h1></h1>
Tournament system meant to be easy to use. Bracket is written in async Python (with
[FastAPI](https://fastapi.tiangolo.com)) and [Next.js](https://nextjs.org/) as frontend using the
[FastAPI](https://fastapi.tiangolo.com)) and [Vite](https://vite.dev/) as frontend using the
[Mantine](https://mantine.dev/) library.
It has the following features:
@@ -104,17 +104,17 @@ Read the [configuration docs](https://docs.bracketapp.nl/docs/running-bracket/co
Bracket's backend is configured using `.env` files (`prod.env` for production, `dev.env` for development etc.).
But you can also configure Bracket using environment variables directly, for example by specifying them in the `docker-compose.yml`.
The frontend doesn't can be configured by environment variables as well, as well as `.env` files using Next.js' way of loading environment variables.
The frontend doesn't can be configured by environment variables as well, as well as `.env` files using Vite's way of loading environment variables.
# Running Bracket in production
Read the [deployment docs](https://docs.bracketapp.nl/docs/deployment) for how to deploy Bracket and run it in production.
Bracket can be run in Docker or by itself (using `uv` and `yarn`).
Bracket can be run in Docker or by itself (using `uv` and `pnpm`).
# Development setup
Read the [development docs](https://docs.bracketapp.nl/docs/community/development) for how to run Bracket for development.
Prerequisites are `yarn`, `postgresql` and `uv` to run the frontend, database and backend.
Prerequisites are `pnpm`, `postgresql` and `uv` to run the frontend, database and backend.
# Translations
Based on your browser settings, your language should be automatically detected and loaded. For now,

View File

@@ -26,8 +26,8 @@ services:
bracket-frontend:
container_name: bracket-frontend
environment:
NEXT_PUBLIC_API_BASE_URL: http://localhost:8400
NEXT_PUBLIC_HCAPTCHA_SITE_KEY: 10000000-ffff-ffff-ffff-000000000001
VITE_API_BASE_URL: http://localhost:8400
VITE_HCAPTCHA_SITE_KEY: 10000000-ffff-ffff-ffff-000000000001
image: ghcr.io/evroon/bracket-frontend
ports:
- 3000:3000

View File

@@ -56,7 +56,7 @@ two sections.
```bash
cd frontend
yarn run dev
pnpm run dev
```
### Backend

View File

@@ -26,8 +26,8 @@ services:
- "3000:3000"
environment:
NODE_ENV: "production"
NEXT_PUBLIC_API_BASE_URL: "http://your-site.com:8400"
NEXT_PUBLIC_HCAPTCHA_SITE_KEY: "10000000-ffff-ffff-ffff-000000000001"
VITE_API_BASE_URL: "http://your-site.com:8400"
VITE_HCAPTCHA_SITE_KEY: "10000000-ffff-ffff-ffff-000000000001"
restart: unless-stopped
bracket-backend:
@@ -63,9 +63,9 @@ Replace the lines that are highlighted in the code block from the previous step.
Replace the following values for `bracket-frontend`:
- `NEXT_PUBLIC_API_BASE_URL`: The address of your backend. The frontend will send
- `VITE_API_BASE_URL`: The address of your backend. The frontend will send
requests to this address.
- `NEXT_PUBLIC_HCAPTCHA_SITE_KEY`: Either leave empty to disable it or
- `VITE_HCAPTCHA_SITE_KEY`: Either leave empty to disable it or
[signup for hCaptcha](https://dashboard.hcaptcha.com/signup), create a site and
put the site key here

View File

@@ -31,12 +31,12 @@ See [the config docs](/docs/running-bracket/configuration.mdx) for more informat
The following configuration variables need to be adjusted for the frontend to run it in production:
- `NEXT_PUBLIC_API_BASE_URL`: The base URL of the backend API to which the frontend sends requests.
- `VITE_API_BASE_URL`: The base URL of the backend API to which the frontend sends requests.
For example: `https://api.bracket.com`
Optional:
- `NEXT_PUBLIC_HCAPTCHA_SITE_KEY`: The HCaptcha key used for captcha challenges when creating new
- `VITE_HCAPTCHA_SITE_KEY`: The HCaptcha key used for captcha challenges when creating new
accounts. You get the secret when you create a new site in HCaptcha. If left blank, HCaptcha will
be disabled for your site.

View File

@@ -89,15 +89,15 @@ job "bracket-frontend" {
driver = "docker"
env {
NEXT_PUBLIC_API_BASE_URL = "https://my.bracketdomain.com"
NEXT_PUBLIC_HCAPTCHA_SITE_KEY = "xxxxx"
VITE_API_BASE_URL = "https://my.bracketdomain.com"
VITE_HCAPTCHA_SITE_KEY = "xxxxx"
NODE_ENV = "production"
}
config {
image = "ghcr.io/evroon/bracket-frontend"
ports = ["nextjs"]
args = ["yarn", "start", "-p", "${NOMAD_PORT_nextjs}"]
args = ["pnpm", "start", "-p", "${NOMAD_PORT_nextjs}"]
}
resources {

View File

@@ -51,7 +51,7 @@ After=network.target
Type=simple
User=bracket
WorkingDirectory=/var/lib/bracket/frontend
ExecStart=/usr/local/bin/yarn start
ExecStart=/usr/local/bin/pnpm start
Environment=NODE_ENV=production
TimeoutSec=15
Restart=always

View File

@@ -5,7 +5,7 @@ title: Introduction
[Bracket](https://github.com/evroon/bracket) is a tournament system meant to be easy to use. Bracket
is written in async Python (with [FastAPI](https://fastapi.tiangolo.com)) and
[Next.js](https://nextjs.org/) as frontend using the [Mantine](https://mantine.dev/) library.
[Vite](https://vite.dev/) as frontend using the [Mantine](https://mantine.dev/) library.
## Overview of features
@@ -36,7 +36,7 @@ tournament systems that exist usually only support Swiss, no other types of tour
(round-robin, elimination etc.). That is quite a limitation when you want to host a tournament that
starts with Swiss and determines a winner based on a knockoff (elimination) stage.
**Finally**, I developed this project to learn more about Next.js and apply my Python (e.g. FastAPI)
**Finally**, I developed this project to learn more about Vite and apply my Python (e.g. FastAPI)
experience to a project with a real purpose.
## Quickstart

View File

@@ -44,28 +44,28 @@ AUTO_RUN_MIGRATIONS=true
## Frontend
- `NEXT_PUBLIC_HCAPTCHA_SITE_KEY`: The HCaptcha key used for captcha challenges when creating new
- `VITE_HCAPTCHA_SITE_KEY`: The HCaptcha key used for captcha challenges when creating new
accounts. You get the secret when you create a new site in HCaptcha.
- `NEXT_PUBLIC_API_BASE_URL`: The base URL of the backend API to which the frontend sends requests.
- `VITE_API_BASE_URL`: The base URL of the backend API to which the frontend sends requests.
For example: `https://api.bracket.com`
- `ANALYTICS_DATA_DOMAIN`: The `data-domain` attribute passed to the script for Plausible
- `VITE_ANALYTICS_DATA_DOMAIN`: The `data-domain` attribute passed to the script for Plausible
analytics
- `ANALYTICS_DATA_WEBSITE_ID`: The `data-website-id` attribute passed to the script for Umami
- `VITE_ANALYTICS_DATA_WEBSITE_ID`: The `data-website-id` attribute passed to the script for Umami
analytics
- `ANALYTICS_SCRIPT_SRC`: The URL to the script for analytics purposes.
- `VITE_ANALYTICS_SCRIPT_SRC`: The URL to the script for analytics purposes.
### Frontend: Example configuration file
You can store the config in `.env.local` (as described in the [Next docs][next-config-url]).
You can store the config in `.env.local` (as described in the [Vite docs][vite-config-url]).
This is an example of how the config file should look like:
```shell
NEXT_PUBLIC_HCAPTCHA_SITE_KEY='10000000-ffff-ffff-ffff-000000000001'
NEXT_PUBLIC_API_BASE_URL='https://api.bracket.com'
ANALYTICS_SCRIPT_SRC='https://analytics.bracket.com/script.js'
ANALYTICS_DATA_DOMAIN='bracket.com'
ANALYTICS_DATA_WEBSITE_ID='bracket.com'
VITE_HCAPTCHA_SITE_KEY='10000000-ffff-ffff-ffff-000000000001'
VITE_API_BASE_URL='https://api.bracket.com'
VITE_ANALYTICS_SCRIPT_SRC='https://analytics.bracket.com/script.js'
VITE_ANALYTICS_DATA_DOMAIN='bracket.com'
VITE_ANALYTICS_DATA_WEBSITE_ID='bracket.com'
```
[next-config-url]: https://nextjs.org/docs/pages/building-your-application/configuring/environment-variables#loading-environment-variables
[vite-config-url]: https://vite.dev/guide/env-and-mode

View File

@@ -10,7 +10,7 @@ title: FAQ
This is likely because you are trying to access Bracket on a different address than
`http://localhost:3000` in the browser. In that case, two things need to be changed:
- Change `NEXT_PUBLIC_API_BASE_URL` to the address of the backend, e.g `https://api.example.org`.
- Change `VITE_API_BASE_URL` to the address of the backend, e.g `https://api.example.org`.
- You will also need to update `CORS_ORIGINS` to the address of the frontend, e.g.
`https://app.example.org`.

View File

@@ -1,4 +1,4 @@
NEXT_PUBLIC_HCAPTCHA_SITE_KEY='10000000-ffff-ffff-ffff-000000000001'
VITE_HCAPTCHA_SITE_KEY='10000000-ffff-ffff-ffff-000000000001'
ANALYTICS_SCRIPT_SRC=''
ANALYTICS_DATA_DOMAIN=''
ANALYTICS_DATA_WEBSITE_ID=''

1
frontend/.gitignore vendored
View File

@@ -14,6 +14,7 @@
# production
/build
/dist
# misc
.DS_Store

View File

@@ -3,11 +3,9 @@ FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json yarn.lock ./
COPY pnpm-lock.yaml package.json ./
# Increase timeout for slow QEMU arm64 builds
# https://github.com/nodejs/docker-node/issues/1335
RUN yarn --network-timeout 1000000
RUN corepack enable && pnpm i
# Rebuild the source code only when needed
FROM node:22-alpine AS builder
@@ -17,9 +15,11 @@ WORKDIR /app
COPY . .
COPY --from=deps /app/node_modules ./node_modules
RUN NEXT_PUBLIC_API_BASE_URL=http://NEXT_PUBLIC_API_BASE_URL_PLACEHOLDER \
NEXT_PUBLIC_HCAPTCHA_SITE_KEY=NEXT_PUBLIC_HCAPTCHA_SITE_KEY_PLACEHOLDER \
yarn build
RUN corepack enable
RUN VITE_API_BASE_URL=http://VITE_API_BASE_URL_PLACEHOLDER \
VITE_HCAPTCHA_SITE_KEY=VITE_HCAPTCHA_SITE_KEY_PLACEHOLDER \
pnpm build
# Production image, copy all the files and run next
FROM node:22-alpine AS runner
@@ -29,27 +29,19 @@ WORKDIR /app
ENV NODE_ENV=production
RUN addgroup -g 1001 --system nodejs && \
adduser --system nextjs -u 1001 -G nodejs
adduser --system vite -u 1001 -G nodejs
COPY --from=builder --chown=nextjs:nodejs /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next ./.next
COPY --from=builder --chown=vite:nodejs /app/public ./public
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./package.json
COPY --from=builder --chmod=0755 /app/docker-entrypoint.sh ./entrypoint.sh
COPY --from=builder /app/next.config.js ./next.config.js
COPY --from=builder /app/next-i18next.config.js ./next-i18next.config.js
RUN apk add bash
RUN yarn next telemetry disable
USER nextjs
USER vite
EXPOSE 3000
ENTRYPOINT ["/app/entrypoint.sh"]
HEALTHCHECK --interval=10s --timeout=5s --retries=5 \
CMD ["wget", "--spider", "http://0.0.0.0:3000", "||", "exit", "1"]
CMD ["yarn", "start"]
CMD ["pnpm", "start"]

View File

@@ -1,23 +0,0 @@
#!/bin/bash
set -eo pipefail
if [ -z ${NEXT_PUBLIC_API_BASE_URL+x} ];
then echo "Environment variable `NEXT_PUBLIC_API_BASE_URL` is not set, please set it in docker-compose.yml";
exit 1;
fi
# Replace the statically built placeholder literals from Dockerfile with run-time
# the value of the `NEXT_PUBLIC_WEBAPP_URL` environment variable
replace_placeholder() {
find .next public -type f |
while read file; do
sed -i "s|$1|$2|g" "$file" || true
done
}
replace_placeholder "http://NEXT_PUBLIC_API_BASE_URL_PLACEHOLDER" "$NEXT_PUBLIC_API_BASE_URL"
replace_placeholder "NEXT_PUBLIC_HCAPTCHA_SITE_KEY_PLACEHOLDER" "$NEXT_PUBLIC_HCAPTCHA_SITE_KEY"
echo "Starting Nextjs"
exec "$@"

18
frontend/i18n.ts Normal file
View File

@@ -0,0 +1,18 @@
import i18n from 'i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import Backend from 'i18next-http-backend';
import { initReactI18next } from 'react-i18next';
i18n
.use(Backend)
.use(LanguageDetector)
.use(initReactI18next)
.init({
fallbackLng: 'en',
defaultNS: 'common',
interpolation: {
escapeValue: false,
},
});
export default i18n;

17
frontend/index.html Normal file
View File

@@ -0,0 +1,17 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Bracket</title>
<meta charSet="UTF-8" />
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="shortcut icon" href="/favicon.svg" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@@ -1,16 +0,0 @@
const nextJest = require('next/jest');
const createJestConfig = nextJest({
dir: './',
});
const customJestConfig = {
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
moduleNameMapper: {
'^@/components/(.*)$': '<rootDir>/components/$1',
'^@/pages/(.*)$': '<rootDir>/pages/$1',
},
testEnvironment: 'jest-environment-jsdom',
};
module.exports = createJestConfig(customJestConfig);

View File

@@ -1 +0,0 @@
import '@testing-library/jest-dom/extend-expect';

View File

@@ -1,5 +0,0 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.

View File

@@ -1,9 +0,0 @@
/** @type {import('next-i18next').UserConfig} */
const path = require('path');
module.exports = {
i18n: {
locales: ['de', 'el', 'en', 'es', 'fr', 'it', 'ja', 'nl', 'pt', 'sv', 'zh'],
defaultLocale: 'en',
},
localePath: path.resolve('./public/locales')
};

View File

@@ -1,12 +0,0 @@
const { i18n } = require('./next-i18next.config.js');
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
});
module.exports = withBundleAnalyzer({
reactStrictMode: false,
eslint: {
ignoreDuringBuilds: true,
},
i18n,
});

View File

@@ -1,86 +1,72 @@
{
"name": "Bracket",
"name": "bracket",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"analyze": "ANALYZE=true next build",
"start": "next start",
"dev": "vite",
"build": "vite build",
"analyze": "ANALYZE=true vite build",
"start": "vite start",
"typecheck": "tsc --noEmit",
"export": "next build && next export",
"lint": "next lint",
"export": "vite build && vite export",
"jest": "jest",
"jest:watch": "jest --watch",
"prettier:check": "prettier --check \"**/*.{ts,tsx}\"",
"prettier:write": "prettier --write \"**/*.{ts,tsx}\"",
"test": "tsc --noEmit && npm run prettier:write && npm run lint && npm run jest -- --passWithNoTests"
"test": "tsc --noEmit && pnpm run prettier:write"
},
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/server": "^11.11.0",
"@hcaptcha/react-hcaptcha": "^1.14.0",
"@hello-pangea/dnd": "^18.0.1",
"@mantine/core": "7.12.1",
"@mantine/dates": "7.12.1",
"@mantine/dropzone": "7.12.1",
"@mantine/form": "8.3.0",
"@mantine/hooks": "7.12.1",
"@mantine/next": "^6.0.21",
"@mantine/notifications": "7.12.1",
"@mantine/spotlight": "7.12.1",
"@next/bundle-analyzer": "^16.0.0",
"@mantine/core": "8.3.7",
"@mantine/dates": "8.3.7",
"@mantine/dropzone": "8.3.7",
"@mantine/form": "8.3.7",
"@mantine/hooks": "8.3.7",
"@mantine/notifications": "8.3.7",
"@mantine/spotlight": "8.3.7",
"@react-icons/all-files": "^4.1.0",
"@tabler/icons-react": "^3.35.0",
"@vitejs/plugin-react": "^5.1.0",
"axios": "^1.13.0",
"clsx": "^2.0.0",
"cookies-next": "^6.1.0",
"date-fns": "^4.1.0",
"dayjs": "^1.11.10",
"i18next": "^25.6.0",
"next": "^15.5.0",
"next-i18next": "^15.4.0",
"nuqs": "^2.7.3",
"react": "^18.3.0",
"react-dom": "^18.3.0",
"react-ellipsis-text": "^1.2.1",
"react-i18next": "^16.2.0",
"react-icons": "^5.5.0",
"react-qr-code": "^2.0.12",
"react-redux": "^9.2.0",
"swr": "^2.3.0",
"yarn-upgrade-all": "^0.7.2"
"react-router": "^7.9.5",
"swr": "^2.3.0"
},
"devDependencies": {
"@babel/core": "^7.28.0",
"@next/eslint-plugin-next": "^15.5.0",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.9.0",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.0",
"@trivago/prettier-plugin-sort-imports": "^6.0.0",
"@types/jest": "^30.0.0",
"@types/node": "^24.10.0",
"@types/react": "^18.3.0",
"@types/react-dom": "^18.3.0",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
"babel-loader": "^10.0.0",
"eslint": "^8.57.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-typescript": "^18.0.0",
"eslint-config-mantine": "^3.2.0",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-jest": "^29.1.0",
"eslint-plugin-jsx-a11y": "^6.10.0",
"eslint-plugin-react": "^7.37.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-testing-library": "^7.13.0",
"jest": "^30.2.0",
"jest-environment-jsdom": "^30.2.0",
"i18next-browser-languagedetector": "^8.2.0",
"i18next-http-backend": "^3.0.2",
"postcss": "^8.5.0",
"postcss-preset-mantine": "^1.18.0",
"postcss-simple-vars": "^7.0.1",
"prettier": "^3.6.0",
"ts-jest": "^29.4.0",
"typescript": "^5.8.2"
"typescript": "^5.8.2",
"vite-plugin-i18next-loader": "^3.1.3",
"vite": "^7.2.2"
}
}

5786
frontend/pnpm-lock.yaml generated Normal file
View File

File diff suppressed because it is too large Load Diff

View File

@@ -2,17 +2,17 @@
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="197.44826"
height="128"
viewBox="0 0 52.241518 33.866667"
version="1.1"
id="svg22318"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
sodipodi:docname="group-stage-item.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
width="197.44826"
height="128"
viewBox="0 0 52.241518 33.866667"
version="1.1"
id="svg22318"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
sodipodi:docname="group-stage-item.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
>
<sodipodi:namedview
id="namedview22320"
pagecolor="#ffffff"

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@@ -2,17 +2,17 @@
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="62.189377mm"
height="40.215961mm"
viewBox="0 0 62.189377 40.215962"
version="1.1"
id="svg25466"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
sodipodi:docname="single-elimination-stage-item.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
width="62.189377mm"
height="40.215961mm"
viewBox="0 0 62.189377 40.215962"
version="1.1"
id="svg25466"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
sodipodi:docname="single-elimination-stage-item.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
>
<sodipodi:namedview
id="namedview25468"
pagecolor="#ffffff"

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

@@ -2,17 +2,17 @@
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="210mm"
height="297mm"
viewBox="0 0 210 297"
version="1.1"
id="svg5"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
sodipodi:docname="stage-items.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
width="210mm"
height="297mm"
viewBox="0 0 210 297"
version="1.1"
id="svg5"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
sodipodi:docname="stage-items.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
>
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -2,17 +2,17 @@
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="200"
height="128"
viewBox="0 0 52.916667 33.866666"
version="1.1"
id="svg24588"
sodipodi:docname="swiss-stage-item.svg"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
width="200"
height="128"
viewBox="0 0 52.916667 33.866666"
version="1.1"
id="svg24588"
sodipodi:docname="swiss-stage-item.svg"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
>
<sodipodi:namedview
id="namedview24590"
pagecolor="#ffffff"

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -12,8 +12,8 @@ import {
import { GoPlus } from '@react-icons/all-files/go/GoPlus';
import { IoOptions } from '@react-icons/all-files/io5/IoOptions';
import { IconAlertCircle } from '@tabler/icons-react';
import { useTranslation } from 'next-i18next';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { MdOutlineAutoFixHigh } from 'react-icons/md';
import { SWRResponse } from 'swr';

View File

@@ -1,8 +1,7 @@
import { Center, Grid, UnstyledButton, useMantineTheme } from '@mantine/core';
import { useColorScheme } from '@mantine/hooks';
import assert from 'assert';
import { useTranslation } from 'next-i18next';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { SWRResponse } from 'swr';
import {
@@ -15,6 +14,7 @@ import { RoundInterface } from '../../interfaces/round';
import { TournamentMinimal } from '../../interfaces/tournament';
import { getMatchLookup, getStageItemLookup } from '../../services/lookups';
import MatchModal from '../modals/match_modal';
import { assert_not_none } from '../utils/assert';
import { Time } from '../utils/datetime';
import classes from './match.module.css';
@@ -99,7 +99,6 @@ export default function Match({
if (readOnly) {
return <div className={classes.root}>{bracket}</div>;
}
assert(swrStagesResponse != null);
return (
<>
@@ -107,7 +106,7 @@ export default function Match({
{bracket}
</UnstyledButton>
<MatchModal
swrStagesResponse={swrStagesResponse}
swrStagesResponse={assert_not_none(swrStagesResponse)}
swrUpcomingMatchesResponse={swrUpcomingMatchesResponse}
tournamentData={tournamentData}
match={match}

View File

@@ -17,10 +17,8 @@ import { useColorScheme } from '@mantine/hooks';
import { AiFillWarning } from '@react-icons/all-files/ai/AiFillWarning';
import { BiCheck } from '@react-icons/all-files/bi/BiCheck';
import { IconDots, IconPencil, IconTrash } from '@tabler/icons-react';
import assert from 'assert';
import { useTranslation } from 'next-i18next';
import Link from 'next/link';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { BiSolidWrench } from 'react-icons/bi';
import { SWRResponse } from 'swr';
@@ -42,7 +40,9 @@ import CreateStageButton from '../buttons/create_stage';
import { CreateStageItemModal } from '../modals/create_stage_item';
import { UpdateStageModal } from '../modals/update_stage';
import { UpdateStageItemModal } from '../modals/update_stage_item';
import { assert_not_none } from '../utils/assert';
import RequestErrorAlert from '../utils/error_alert';
import PreloadLink from '../utils/link';
import { responseIsValid } from '../utils/util';
function StageItemInputComboBox({
@@ -163,7 +163,7 @@ export function getAvailableInputs(
) {
const getComboBoxOptionForStageItemInput = (option: StageItemInputOption) => {
if (option.winner_from_stage_item_id != null) {
assert(option.winner_position != null);
option.winner_position = assert_not_none(option.winner_position);
const stageItem = stageItemMap[option.winner_from_stage_item_id];
if (stageItem == null) return null;
@@ -180,9 +180,8 @@ export function getAvailableInputs(
if (option.team_id == null) return null;
const team = teamsMap[option.team_id];
if (team == null) return null;
assert(option.team_id === team.id);
return {
value: `${option.team_id}`,
value: `${assert_not_none(option.team_id)}`,
label: team.name,
team_id: team.id,
winner_from_stage_item_id: null,
@@ -301,7 +300,7 @@ function StageItemRow({
<ActionIcon
variant="transparent"
color="gray"
component={Link}
component={PreloadLink}
href={`/tournaments/${tournament.id}/stages/swiss/${stageItem.id}`}
>
<BiSolidWrench size="1.25rem" />
@@ -327,7 +326,7 @@ function StageItemRow({
{stageItem.type === 'SWISS' ? (
<Menu.Item
leftSection={<BiSolidWrench size="1.5rem" />}
component={Link}
component={PreloadLink}
href={`/tournaments/${tournament.id}/stages/swiss/${stageItem.id}`}
>
{t('handle_swiss_system')}

View File

@@ -1,7 +1,7 @@
import { Button } from '@mantine/core';
import { GoPlus } from '@react-icons/all-files/go/GoPlus';
import { useTranslation } from 'next-i18next';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { SWRResponse } from 'swr';
import { Tournament } from '../../interfaces/tournament';

View File

@@ -1,7 +1,6 @@
import { Badge, Button, Card, Group, Image, Text, UnstyledButton } from '@mantine/core';
import { useTranslation } from 'next-i18next';
import Link from 'next/link';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { SWRResponse } from 'swr';
import { Tournament } from '../../interfaces/tournament';
@@ -9,6 +8,7 @@ import { getBaseApiUrl } from '../../services/adapter';
import { EmptyTableInfo } from '../no_content/empty_table_info';
import { DateTime } from '../utils/datetime';
import RequestErrorAlert from '../utils/error_alert';
import PreloadLink from '../utils/link';
import { TableSkeletonSingleColumn } from '../utils/skeletons';
import classes from './tournaments.module.css';
@@ -58,7 +58,11 @@ export default function TournamentsCardTable({
.sort((t1: Tournament, t2: Tournament) => t1.name.localeCompare(t2.name))
.map((tournament) => (
<Group key={tournament.id} className={classes.card}>
<UnstyledButton component={Link} href={`/tournaments/${tournament.id}/stages`} w="100%">
<UnstyledButton
component={PreloadLink}
href={`/tournaments/${tournament.id}/stages`}
w="100%"
>
<Card shadow="sm" padding="lg" radius="md" withBorder w="100%">
<Card.Section>
<TournamentLogo tournament={tournament} />
@@ -86,7 +90,7 @@ export default function TournamentsCardTable({
{t('archived_label')}
</Badge>
<Button
component={Link}
component={PreloadLink}
color="blue"
fullWidth
radius="md"

View File

@@ -8,13 +8,13 @@ import {
Title,
UnstyledButton,
} from '@mantine/core';
import Link from 'next/link';
import { useRouter } from 'next/router';
import React from 'react';
import QRCode from 'react-qr-code';
import { useLocation, useNavigate } from 'react-router';
import { Tournament } from '../../interfaces/tournament';
import { getBaseApiUrl } from '../../services/adapter';
import PreloadLink from '../utils/link';
import { getBaseURL } from '../utils/util';
import classes from './layout.module.css';
@@ -63,12 +63,8 @@ export function TournamentLogo({ tournamentDataFull }: { tournamentDataFull: Tou
) : null;
}
export function TournamentHeadTitle({ tournamentDataFull }: { tournamentDataFull: Tournament }) {
return tournamentDataFull != null ? (
<title>{tournamentDataFull.name}</title>
) : (
<title>Bracket</title>
);
export function getTournamentHeadTitle(tournamentDataFull: Tournament) {
return tournamentDataFull !== null ? `Bracket | ${tournamentDataFull.name}` : 'Bracket';
}
export function TournamentTitle({ tournamentDataFull }: { tournamentDataFull: Tournament }) {
@@ -80,9 +76,9 @@ export function TournamentTitle({ tournamentDataFull }: { tournamentDataFull: To
}
export function DoubleHeader({ tournamentData }: { tournamentData: Tournament }) {
const router = useRouter();
const navigate = useLocation();
const endpoint = tournamentData.dashboard_endpoint;
const pathName = router.pathname.replace('[id]', endpoint).replace(/\/+$/, '');
const pathName = navigate.pathname.replace('[id]', endpoint).replace(/\/+$/, '');
const mainLinks = [
{ link: `/tournaments/${endpoint}/dashboard`, label: 'Matches' },
@@ -90,20 +86,20 @@ export function DoubleHeader({ tournamentData }: { tournamentData: Tournament })
];
const mainItems = mainLinks.map((item) => (
<Link
<PreloadLink
href={item.link}
key={item.label}
className={classes.mainLink}
data-active={item.link === pathName || undefined}
>
{item.label}
</Link>
</PreloadLink>
));
return (
<header className={classes.header}>
<Container className={classes.inner}>
<UnstyledButton component={Link} href={`/tournaments/${endpoint}/dashboard`}>
<UnstyledButton component={PreloadLink} href={`/tournaments/${endpoint}/dashboard`}>
<Title size="lg" lineClamp={1}>
{tournamentData.name}
</Title>

View File

@@ -1,7 +1,7 @@
import { Code, Text, Textarea } from '@mantine/core';
import { UseFormReturnType } from '@mantine/form';
import { useTranslation } from 'next-i18next';
import React from 'react';
import { useTranslation } from 'react-i18next';
export function MultiPlayersInput({ form }: { form: UseFormReturnType<any> }) {
const { t } = useTranslation();

View File

@@ -2,9 +2,8 @@ import { Button, Select, Tabs, TextInput } from '@mantine/core';
import { useForm } from '@mantine/form';
import { BiGlobe } from '@react-icons/all-files/bi/BiGlobe';
import { IconHash, IconLogout, IconUser } from '@tabler/icons-react';
import assert from 'assert';
import { useRouter } from 'next/router';
import React from 'react';
import { useNavigate } from 'react-router';
import { UserInterface } from '../../interfaces/user';
import { performLogoutAndRedirect } from '../../services/local_storage';
@@ -12,7 +11,7 @@ import { updatePassword, updateUser } from '../../services/user';
import { PasswordStrength } from '../utils/password';
export default function UserForm({ user, t, i18n }: { user: UserInterface; t: any; i18n: any }) {
const router = useRouter();
const navigate = useNavigate();
const details_form = useForm({
initialValues: {
name: user != null ? user.name : '',
@@ -50,9 +49,8 @@ export default function UserForm({ user, t, i18n }: { user: UserInterface; t: an
];
const changeLanguage = (newLocale: string | null) => {
const { pathname, asPath, query } = router;
assert(newLocale != null);
router.push({ pathname, query }, asPath, { locale: newLocale });
i18n.changeLanguage(newLocale);
navigate(`/user?lng=${newLocale}`);
};
return (
@@ -96,7 +94,7 @@ export default function UserForm({ user, t, i18n }: { user: UserInterface; t: an
color="red"
variant="outline"
leftSection={<IconLogout />}
onClick={() => performLogoutAndRedirect(t, router)}
onClick={() => performLogoutAndRedirect(t, navigate)}
>
{t('logout_title')}
</Button>

View File

@@ -1,8 +1,8 @@
import { Alert, Button, Checkbox, Modal } from '@mantine/core';
import { useForm } from '@mantine/form';
import { IconAlertCircle, IconSquareArrowRight } from '@tabler/icons-react';
import { useTranslation } from 'next-i18next';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { MdOutlineAutoFixHigh } from 'react-icons/md';
import { SWRResponse } from 'swr';

View File

@@ -2,8 +2,8 @@ import { Alert, Button, Container, Grid, Modal, Title } from '@mantine/core';
import { useForm } from '@mantine/form';
import { FaArrowRight } from '@react-icons/all-files/fa/FaArrowRight';
import { IconAlertCircle, IconSquareArrowRight } from '@tabler/icons-react';
import { useTranslation } from 'next-i18next';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { SWRResponse } from 'swr';
import { StageItemWithRounds } from '../../interfaces/stage_item';

View File

@@ -1,8 +1,8 @@
import { Alert, Button, Modal } from '@mantine/core';
import { useForm } from '@mantine/form';
import { IconAlertCircle, IconSquareArrowLeft } from '@tabler/icons-react';
import { useTranslation } from 'next-i18next';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { SWRResponse } from 'swr';
import { activateNextStage } from '../../services/stage';

View File

@@ -2,8 +2,8 @@ import { Button, Modal, TextInput } from '@mantine/core';
import { useForm } from '@mantine/form';
import { BiEditAlt } from '@react-icons/all-files/bi/BiEditAlt';
import { GoPlus } from '@react-icons/all-files/go/GoPlus';
import { useTranslation } from 'next-i18next';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { SWRResponse } from 'swr';
import { Club } from '../../interfaces/club';

View File

@@ -1,8 +1,8 @@
import { Button, Modal, TextInput } from '@mantine/core';
import { useForm } from '@mantine/form';
import { GoPlus } from '@react-icons/all-files/go/GoPlus';
import { useTranslation } from 'next-i18next';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { SWRResponse } from 'swr';
import { createCourt } from '../../services/court';

View File

@@ -12,8 +12,8 @@ import {
} from '@mantine/core';
import { UseFormReturnType, useForm } from '@mantine/form';
import { GoPlus } from '@react-icons/all-files/go/GoPlus';
import { useTranslation } from 'next-i18next';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { SWRResponse } from 'swr';
import { StageWithStageItems } from '../../interfaces/stage';

View File

@@ -1,7 +1,7 @@
import { Button, Center, Checkbox, Divider, Grid, Modal, NumberInput, Text } from '@mantine/core';
import { useForm } from '@mantine/form';
import { useTranslation } from 'next-i18next';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { SWRResponse } from 'swr';
import {

View File

@@ -1,8 +1,8 @@
import { Button, Checkbox, Modal, Tabs, TextInput } from '@mantine/core';
import { useForm } from '@mantine/form';
import { IconUser, IconUserPlus, IconUsers } from '@tabler/icons-react';
import { useTranslation } from 'next-i18next';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { SWRResponse } from 'swr';
import { createMultiplePlayers, createPlayer } from '../../services/player';

View File

@@ -1,8 +1,8 @@
import { Button, Checkbox, Modal, TextInput } from '@mantine/core';
import { useForm } from '@mantine/form';
import { BiEditAlt } from '@react-icons/all-files/bi/BiEditAlt';
import { useTranslation } from 'next-i18next';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { SWRResponse } from 'swr';
import { Player } from '../../interfaces/player';

View File

@@ -1,8 +1,8 @@
import { ActionIcon, Button, Modal, TextInput, Title, UnstyledButton } from '@mantine/core';
import { useForm } from '@mantine/form';
import { IconPencil } from '@tabler/icons-react';
import { useTranslation } from 'next-i18next';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { LuConstruction } from 'react-icons/lu';
import { SWRResponse } from 'swr';

View File

@@ -12,15 +12,15 @@ import {
IconUsers,
IconUsersGroup,
} from '@tabler/icons-react';
import { useTranslation } from 'next-i18next';
import { useRouter } from 'next/router';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router';
import { getTournamentIdFromRouter } from '../utils/util';
export function BracketSpotlight() {
const { t } = useTranslation();
const router = useRouter();
const navigate = useNavigate();
const { id: tournamentId } = getTournamentIdFromRouter();
const actions: SpotlightActionData[] = [
@@ -28,21 +28,21 @@ export function BracketSpotlight() {
id: 'home',
title: t('home_title'),
description: t('home_spotlight_description'),
onClick: () => router.push('/'),
onClick: () => navigate('/'),
leftSection: <IconHome size="1.2rem" />,
},
{
id: 'clubs',
title: t('clubs_title'),
description: t('clubs_spotlight_description'),
onClick: () => router.push('/clubs'),
onClick: () => navigate('/clubs'),
leftSection: <IconUsersGroup size="1.2rem" />,
},
{
id: 'user settings',
title: t('user_settings_title'),
description: t('user_settings_spotlight_description'),
onClick: () => router.push('/user'),
onClick: () => navigate('/user'),
leftSection: <IconUser size="1.2rem" />,
},
];
@@ -52,49 +52,49 @@ export function BracketSpotlight() {
id: 'results',
title: t('results_title'),
description: t('results_spotlight_description'),
onClick: () => router.push(`/tournaments/${tournamentId}/results`),
onClick: () => navigate(`/tournaments/${tournamentId}/results`),
leftSection: <IconBrackets size="1.2rem" />,
},
{
id: 'planning',
title: t('planning_title'),
description: t('planning_spotlight_description'),
onClick: () => router.push(`/tournaments/${tournamentId}/schedule`),
onClick: () => navigate(`/tournaments/${tournamentId}/schedule`),
leftSection: <IconCalendarEvent size="1.2rem" />,
},
{
id: 'teams',
title: t('teams_title'),
description: t('teams_spotlight_description'),
onClick: () => router.push(`/tournaments/${tournamentId}/teams`),
onClick: () => navigate(`/tournaments/${tournamentId}/teams`),
leftSection: <IconUsers size="1.2rem" />,
},
{
id: 'players',
title: t('players_title'),
description: t('players_spotlight_description'),
onClick: () => router.push(`/tournaments/${tournamentId}/players`),
onClick: () => navigate(`/tournaments/${tournamentId}/players`),
leftSection: <IconUsers size="1.2rem" />,
},
{
id: 'stages',
title: t('stage_title'),
description: t('stage_spotlight_description'),
onClick: () => router.push(`/tournaments/${tournamentId}/stages`),
onClick: () => navigate(`/tournaments/${tournamentId}/stages`),
leftSection: <IconTrophy size="1.2rem" />,
},
{
id: 'tournament settings',
title: t('tournament_setting_title'),
description: t('tournament_setting_spotlight_description'),
onClick: () => router.push(`/tournaments/${tournamentId}/settings`),
onClick: () => navigate(`/tournaments/${tournamentId}/settings`),
leftSection: <IconSettings size="1.2rem" />,
},
{
id: 'rankings',
title: t('rankings_title'),
description: t('rankings_spotlight_description'),
onClick: () => router.push(`/tournaments/${tournamentId}/rankings`),
onClick: () => navigate(`/tournaments/${tournamentId}/rankings`),
leftSection: <IconScoreboard size="1.2rem" />,
},
];

View File

@@ -1,8 +1,8 @@
import { Button, Checkbox, Modal, MultiSelect, Tabs, TextInput } from '@mantine/core';
import { useForm } from '@mantine/form';
import { IconUser, IconUsers, IconUsersPlus } from '@tabler/icons-react';
import { useTranslation } from 'next-i18next';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { SWRResponse } from 'swr';
import { Player } from '../../interfaces/player';

View File

@@ -10,8 +10,8 @@ import {
} from '@mantine/core';
import { useForm } from '@mantine/form';
import { BiEditAlt } from '@react-icons/all-files/bi/BiEditAlt';
import { useTranslation } from 'next-i18next';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { SWRResponse } from 'swr';
import { Player } from '../../interfaces/player';

View File

@@ -12,9 +12,8 @@ import { DateTimePicker } from '@mantine/dates';
import { useForm } from '@mantine/form';
import { GoPlus } from '@react-icons/all-files/go/GoPlus';
import { IconCalendar, IconCalendarTime } from '@tabler/icons-react';
import assert from 'assert';
import { useTranslation } from 'next-i18next';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { SWRResponse } from 'swr';
import { Club } from '../../interfaces/club';
@@ -22,6 +21,7 @@ import { Tournament } from '../../interfaces/tournament';
import { getBaseApiUrl, getClubs } from '../../services/adapter';
import { createTournament } from '../../services/tournament';
import SaveButton from '../buttons/save';
import { assert_not_none } from '../utils/assert';
export function TournamentLogo({ tournament }: { tournament: Tournament | null }) {
if (tournament == null || tournament.logo_path == null) return null;
@@ -71,9 +71,8 @@ function GeneralTournamentForm({
return (
<form
onSubmit={form.onSubmit(async (values) => {
assert(values.club_id != null);
await createTournament(
parseInt(values.club_id, 10),
parseInt(assert_not_none(values.club_id), 10),
values.name,
values.dashboard_public,
values.dashboard_endpoint,

View File

@@ -1,7 +1,7 @@
import { Button, Modal, TextInput } from '@mantine/core';
import { useForm } from '@mantine/form';
import { useTranslation } from 'next-i18next';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { SWRResponse } from 'swr';
import { StageWithStageItems } from '../../interfaces/stage';

View File

@@ -1,7 +1,7 @@
import { Button, Modal, TextInput } from '@mantine/core';
import { useForm } from '@mantine/form';
import { useTranslation } from 'next-i18next';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { SWRResponse } from 'swr';
import { Ranking } from '../../interfaces/ranking';

View File

@@ -1,11 +1,12 @@
import { Center, Group, Image, Text, Title, UnstyledButton } from '@mantine/core';
import Link from 'next/link';
import React from 'react';
import PreloadLink from '../utils/link';
export function Brand() {
return (
<Center mr="1rem" miw="12rem">
<UnstyledButton component={Link} href="/">
<UnstyledButton component={PreloadLink} href="/">
<Group>
<Image
style={{ width: '38px', marginRight: '0px' }}

View File

@@ -14,12 +14,12 @@ import {
IconUser,
IconUsers,
} from '@tabler/icons-react';
import { useTranslation } from 'next-i18next';
import Link from 'next/link';
import { useRouter } from 'next/router';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useLocation } from 'react-router';
import { getBaseApiUrl } from '../../services/adapter';
import PreloadLink from '../utils/link';
import { capitalize } from '../utils/util';
import classes from './_main_links.module.css';
@@ -35,7 +35,7 @@ function MainLinkMobile({ item, pathName }: { item: MainLinkProps; pathName: Str
<>
<UnstyledButton
hiddenFrom="sm"
component={Link}
component={PreloadLink}
href={item.link}
className={classes.mobileLink}
style={{ width: '100%' }}
@@ -57,7 +57,7 @@ function MainLink({ item, pathName }: { item: MainLinkProps; pathName: String })
<Tooltip position="right" label={item.label} transitionProps={{ duration: 0 }}>
<UnstyledButton
visibleFrom="sm"
component={Link}
component={PreloadLink}
href={item.link}
className={classes.link}
data-active={pathName.startsWith(item.link) || undefined}
@@ -100,18 +100,18 @@ export function getBaseLinksDict() {
}
export function getBaseLinks() {
const router = useRouter();
const pathName = router.pathname.replace(/\/+$/, '');
const location = useLocation();
const pathName = location.pathname.replace(/\/+$/, '');
return getBaseLinksDict()
.filter((link) => link.links.length < 1)
.map((link) => <MainLinkMobile key={link.label} item={link} pathName={pathName} />);
}
export function TournamentLinks({ tournament_id }: any) {
const router = useRouter();
const location = useLocation();
const { t } = useTranslation();
const tm_prefix = `/tournaments/${tournament_id}`;
const pathName = router.pathname.replace('[id]', tournament_id).replace(/\/+$/, '');
const pathName = location.pathname.replace('[id]', tournament_id).replace(/\/+$/, '');
const data = [
{

View File

@@ -1,7 +1,7 @@
import { Alert, Container, Text, Title } from '@mantine/core';
import { IconAlertCircle } from '@tabler/icons-react';
import { useTranslation } from 'next-i18next';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { HiMiniWrenchScrewdriver } from 'react-icons/hi2';
import classes from './empty_table_info.module.css';

View File

@@ -1,7 +1,7 @@
import { Divider, Flex, Group, NumberInput, Progress, Radio, Stack } from '@mantine/core';
import { IconListNumbers, IconMedal, IconRepeat } from '@tabler/icons-react';
import { useTranslation } from 'next-i18next';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { SWRResponse } from 'swr';
import { SchedulerSettings } from '../../../interfaces/match';

View File

@@ -1,6 +1,6 @@
import { Select } from '@mantine/core';
import { useTranslation } from 'next-i18next';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Ranking } from '../../interfaces/ranking';

View File

@@ -1,6 +1,6 @@
import { Table } from '@mantine/core';
import { useTranslation } from 'next-i18next';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { SWRResponse } from 'swr';
import { Club } from '../../interfaces/club';

View File

@@ -1,6 +1,6 @@
import { Badge, Center, Pagination, Table, Text } from '@mantine/core';
import { useTranslation } from 'next-i18next';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { SWRResponse } from 'swr';
import { Player } from '../../interfaces/player';

View File

@@ -1,6 +1,6 @@
import { Table, Text } from '@mantine/core';
import { useTranslation } from 'next-i18next';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { StageItemWithRounds } from '../../interfaces/stage_item';
import { StageItemInputFinal, formatStageItemInput } from '../../interfaces/stage_item_input';

View File

@@ -1,6 +1,6 @@
import { Badge, Center, Pagination, Table } from '@mantine/core';
import { useTranslation } from 'next-i18next';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { SWRResponse } from 'swr';
import { TeamInterface } from '../../interfaces/team';

View File

@@ -1,8 +1,8 @@
import { Badge, Button, Center, Stack, Table } from '@mantine/core';
import { GoChecklist } from '@react-icons/all-files/go/GoChecklist';
import { IconCalendarPlus, IconCheck } from '@tabler/icons-react';
import { useTranslation } from 'next-i18next';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { FaCheck } from 'react-icons/fa6';
import { SWRResponse } from 'swr';

View File

@@ -0,0 +1,6 @@
export function assert_not_none<T>(value: T | null) {
if (value === null) {
throw new Error('Assertion failed');
}
return value;
}

View File

@@ -2,8 +2,8 @@ import { Group, Text } from '@mantine/core';
import { Dropzone, MIME_TYPES } from '@mantine/dropzone';
import { IconCloudUpload, IconDownload, IconX } from '@tabler/icons-react';
import { AxiosError } from 'axios';
import { useTranslation } from 'next-i18next';
import { useMemo, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { SWRResponse } from 'swr';
import { TeamInterface } from '../../interfaces/team';

View File

@@ -0,0 +1,12 @@
import type { ButtonProps, ElementProps } from '@mantine/core';
import { forwardRef } from 'react';
import { Link } from 'react-router';
interface MyButtonProps extends ButtonProps, ElementProps<'a', keyof ButtonProps> {}
const PreloadLink = forwardRef(function PreloadLink({ href, ...props }: MyButtonProps, ref) {
// @ts-ignore
return <Link to={href} ref={ref} {...props}></Link>;
});
export default PreloadLink;

View File

@@ -1,6 +1,6 @@
import { Group, PasswordInput, Progress, Stack, Text } from '@mantine/core';
import { IconCheck, IconX } from '@tabler/icons-react';
import { useTranslation } from 'next-i18next';
import { useTranslation } from 'react-i18next';
function PasswordRequirement({ meets, label }: { meets: boolean; label: string }) {
return (

View File

@@ -1,87 +0,0 @@
import { useRouter } from 'next/router';
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
import { useWatch } from './watch';
type SerializerFunction = (value: any) => string | undefined;
type DeserializerFunction = (value: string) => any;
interface Options {
serializer?: SerializerFunction;
deserializer?: DeserializerFunction;
}
export function useRouterReady() {
const [isReady, setIsReady] = useState(false);
const router = useRouter();
useEffect(() => {
setIsReady(router.isReady);
}, [router.isReady]);
return isReady;
}
export function useRouterQueryState<T>(
name: string,
defaultValue?: T,
opts: Options = {}
): [T, Dispatch<SetStateAction<T>>] {
const router = useRouter();
const serialize = (value: T): string | undefined => {
if (opts.serializer) {
return opts.serializer(value);
}
return value as string;
};
const deserialize = (value: string): T => {
if (opts.deserializer) return opts.deserializer(value);
// default deserializer for number type
if (typeof defaultValue === 'number') {
const numValue = Number(value === '' ? 'r' : value);
return Number.isNaN(numValue) ? (defaultValue as T) : (numValue as T);
}
return value as T;
};
const [state, setState] = useState<T>(() => {
const value = router.query[name];
if (value === undefined) {
return defaultValue as T;
}
return deserialize(value as string);
});
useWatch(() => {
//! Don't manipulate the query parameter directly
const serializedState = serialize(state);
const _q = router.query;
if (serializedState === undefined) {
if (router.query[name]) {
delete _q[name];
router.query = _q;
}
} else {
_q[name] = serializedState;
router.query = _q;
}
router.push(
{
pathname: window.location.pathname,
query: {
..._q,
[name]: router.query[name],
},
hash: window.location.hash,
},
undefined,
{ shallow: true }
);
}, [state, name]);
return [state, setState];
}

View File

@@ -1,7 +1,7 @@
import HCaptcha from '@hcaptcha/react-hcaptcha';
import { Center } from '@mantine/core';
import { useRouter } from 'next/router';
import React from 'react';
import { useParams } from 'react-router';
import { SWRResponse } from 'swr';
import classes from '../../pages/create_account.module.css';
@@ -37,22 +37,22 @@ export function getDefaultTimeRange(selectMultipleDates: boolean) {
}
export function getTournamentIdFromRouter() {
const router = useRouter();
const { id: idString }: any = router.query;
const params = useParams();
const { id: idString }: any = params;
const id = parseInt(idString, 10);
const tournamentData = { id };
return { id, tournamentData };
}
export function getStageItemIdFromRouter() {
const router = useRouter();
const { stage_item_id: idString }: any = router.query;
const params = useParams();
const { stage_item_id: idString }: any = params;
return parseInt(idString, 10);
}
export function getTournamentEndpointFromRouter() {
const router = useRouter();
const { id }: any = router.query;
const params = useParams();
const { id }: any = params;
return id;
}
@@ -100,3 +100,7 @@ export interface Pagination {
sort_by: string;
sort_direction: 'asc' | 'desc';
}
export const setTitle = (title: string) => {
document.title = title;
};

View File

@@ -1,5 +1,4 @@
import assert from 'assert';
import { assert_not_none } from '../components/utils/assert';
import { TeamInterface } from './team';
export interface StageItemInput {
@@ -70,10 +69,7 @@ export function formatStageItemInputTentative(
stage_item_input: StageItemInput | StageItemInputOption,
stageItemsLookup: any
) {
assert(
stage_item_input.winner_from_stage_item_id != null && stage_item_input.winner_position != null
);
return `${getPositionName(stage_item_input.winner_position)} of ${stageItemsLookup[stage_item_input.winner_from_stage_item_id].name}`;
return `${getPositionName(assert_not_none(stage_item_input.winner_position))} of ${stageItemsLookup[assert_not_none(stage_item_input.winner_from_stage_item_id)].name}`;
}
export function formatStageItemInput(
@@ -83,7 +79,7 @@ export function formatStageItemInput(
if (stage_item_input == null) return null;
if (stage_item_input?.team != null) return stage_item_input.team.name;
if (stage_item_input?.winner_from_stage_item_id != null) {
assert(stage_item_input.winner_position != null);
assert_not_none(stage_item_input.winner_position);
return formatStageItemInputTentative(stage_item_input, stageItemsLookup);
}
return null;

115
frontend/src/main.tsx Normal file
View File

@@ -0,0 +1,115 @@
import { MantineProvider, createTheme } from '@mantine/core';
import '@mantine/core/styles.css';
import '@mantine/dates/styles.css';
import '@mantine/dropzone/styles.css';
import { Notifications } from '@mantine/notifications';
import '@mantine/notifications/styles.css';
import '@mantine/spotlight/styles.css';
import { NuqsAdapter } from 'nuqs/adapters/react-router/v7';
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { I18nextProvider } from 'react-i18next';
import { BrowserRouter, Route, Routes } from 'react-router';
import i18n from '../i18n';
import { BracketSpotlight } from './components/modals/spotlight';
import HomePage from './pages';
import NotFoundPage from './pages/404';
import ClubsPage from './pages/clubs';
import CreateAccountPage from './pages/create_account';
import CreateDemoAccountPage from './pages/demo';
import LoginPage from './pages/login';
import PasswordResetPage from './pages/password_reset';
import DashboardSchedulePage from './pages/tournaments/[id]/dashboard';
import CourtsPresentPage from './pages/tournaments/[id]/dashboard/present/courts';
import StandingsPresentPage from './pages/tournaments/[id]/dashboard/present/standings';
import DashboardStandingsPage from './pages/tournaments/[id]/dashboard/standings';
import PlayersPage from './pages/tournaments/[id]/players';
import RankingsPage from './pages/tournaments/[id]/rankings';
import ResultsPage from './pages/tournaments/[id]/results';
import SchedulePage from './pages/tournaments/[id]/schedule';
import SettingsPage from './pages/tournaments/[id]/settings';
import StagesPage from './pages/tournaments/[id]/stages';
import SwissTournamentPage from './pages/tournaments/[id]/stages/swiss/[stage_item_id]';
import TeamsPage from './pages/tournaments/[id]/teams';
import UserPage from './pages/user';
const theme = createTheme({
colors: {
dark: [
'#C1C2C5',
'#A6A7AB',
'#909296',
'#5c5f66',
'#373A40',
'#2C2E33',
'#25262b',
'#1A1B1E',
'#141517',
'#101113',
],
},
});
function AnalyticsScript() {
if (import.meta.env.VITE_ANALYTICS_SCRIPT_SRC == null) {
return null;
}
var script = document.createElement('script');
script.setAttribute('async', '');
script.setAttribute('data-domain', import.meta.env.VITE_ANALYTICS_DATA_DOMAIN);
script.setAttribute('data-website-id', import.meta.env.VITE_ANALYTICS_DATA_WEBSITE_ID);
script.setAttribute('src', import.meta.env.VITE_ANALYTICS_SCRIPT_SRC);
document.head.appendChild(script);
}
createRoot(document.getElementById('root')!).render(
<StrictMode>
<NuqsAdapter>
<BrowserRouter>
<I18nextProvider i18n={i18n}>
<MantineProvider defaultColorScheme="auto" theme={theme}>
<BracketSpotlight />
<Notifications />
<Routes>
<Route index element={<HomePage />} />
<Route path="/login" element={<LoginPage />} />
<Route path="/clubs" element={<ClubsPage />} />
<Route path="/demo" element={<CreateDemoAccountPage />} />
<Route path="/user" element={<UserPage />} />
<Route path="/password-reset" element={<PasswordResetPage />} />
<Route path="/create-account" element={<CreateAccountPage />} />
<Route path="/tournaments">
<Route path=":id">
<Route path="players" element={<PlayersPage />} />
<Route path="teams" element={<TeamsPage />} />
<Route path="schedule" element={<SchedulePage />} />
<Route path="rankings" element={<RankingsPage />} />
<Route path="settings" element={<SettingsPage />} />
<Route path="results" element={<ResultsPage />} />
<Route path="stages">
<Route index element={<StagesPage />} />
<Route path="swiss/:stage_item_id" element={<SwissTournamentPage />} />
</Route>
<Route path="dashboard">
<Route index element={<DashboardSchedulePage />} />
<Route path="standings" element={<DashboardStandingsPage />} />
<Route path="present">
<Route path="courts" element={<CourtsPresentPage />} />
<Route path="standings" element={<StandingsPresentPage />} />
</Route>
</Route>
</Route>
</Route>
<Route path="*" element={<NotFoundPage />} />
</Routes>
</MantineProvider>
</I18nextProvider>
</BrowserRouter>
</NuqsAdapter>
</StrictMode>
);
AnalyticsScript();

View File

@@ -1,29 +1,11 @@
import { Button, Container, Group, Text, Title } from '@mantine/core';
import { GetStaticProps } from 'next';
import { SSRConfig, i18n as globali18n, useTranslation } from 'next-i18next';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router';
import classes from './404.module.css';
export default function NotFoundTitle(props: SSRConfig) {
const router = useRouter();
const [lastData, setLastData] = useState<SSRConfig>();
useEffect(() => {
if (!props._nextI18Next) {
if (lastData?._nextI18Next?.initialI18nStore) {
Object.keys(lastData._nextI18Next.initialI18nStore).forEach((l) => {
Object.keys(lastData?._nextI18Next?.initialI18nStore[l]).forEach((n) => {
globali18n?.addResourceBundle(l, n, lastData?._nextI18Next?.initialI18nStore[l][n]);
});
});
globali18n?.changeLanguage(lastData._nextI18Next.initialLocale);
}
return;
}
setLastData(props);
}, [props]);
export default function NotFoundPage() {
const navigate = useNavigate();
const { t } = useTranslation();
return (
@@ -34,16 +16,10 @@ export default function NotFoundTitle(props: SSRConfig) {
{t('not_found_description')}
</Text>
<Group justify="center">
<Button variant="subtle" size="md" onClick={() => router.push('/')}>
<Button variant="subtle" size="md" onClick={() => navigate('/')}>
{t('back_home_nav')}
</Button>
</Group>
</Container>
);
}
type Props = {};
export const getStaticProps: GetStaticProps<Props> = async ({ locale }) => ({
props: {
...(await serverSideTranslations(locale ?? 'en', ['common'])),
},
});

View File

@@ -1,66 +0,0 @@
import { ColorSchemeScript, MantineProvider, createTheme } from '@mantine/core';
import '@mantine/core/styles.css';
import '@mantine/dates/styles.css';
import '@mantine/dropzone/styles.css';
import { Notifications } from '@mantine/notifications';
import '@mantine/notifications/styles.css';
import '@mantine/spotlight/styles.css';
import { appWithTranslation } from 'next-i18next';
import Head from 'next/head';
import { BracketSpotlight } from '../components/modals/spotlight';
const theme = createTheme({
colors: {
dark: [
'#C1C2C5',
'#A6A7AB',
'#909296',
'#5c5f66',
'#373A40',
'#2C2E33',
'#25262b',
'#1A1B1E',
'#141517',
'#101113',
],
},
});
function AnalyticsScript() {
if (process.env.ANALYTICS_SCRIPT_SRC == null) {
return null;
}
return (
<script
async
data-domain={process.env.ANALYTICS_DATA_DOMAIN}
data-website-id={process.env.ANALYTICS_DATA_WEBSITE_ID}
src={process.env.ANALYTICS_SCRIPT_SRC}
/>
);
}
const App = ({ Component, pageProps }: any) => (
<>
<Head>
<title>Bracket</title>
<meta charSet="UTF-8" />
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="shortcut icon" href="/favicon.svg" />
<AnalyticsScript />
<ColorSchemeScript defaultColorScheme="auto" />
</Head>
<MantineProvider defaultColorScheme="auto" theme={theme}>
<BracketSpotlight />
<Notifications />
<Component {...pageProps} />
</MantineProvider>
</>
);
export default appWithTranslation(App);

View File

@@ -1,8 +0,0 @@
import { createGetInitialProps } from '@mantine/next';
import Document from 'next/document';
const getInitialProps = createGetInitialProps();
export default class _Document extends Document {
static getInitialProps = getInitialProps;
}

View File

@@ -11,12 +11,12 @@ import {
} from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import { Icon, IconMoonStars, IconSun } from '@tabler/icons-react';
import Link from 'next/link';
import { useRouter } from 'next/router';
import React, { ReactNode } from 'react';
import { useLocation } from 'react-router';
import { Brand } from '../components/navbar/_brand';
import { getBaseLinks, getBaseLinksDict } from '../components/navbar/_main_links';
import PreloadLink from '../components/utils/link';
import classes from './_layout.module.css';
interface HeaderActionLink {
@@ -44,13 +44,13 @@ function getMenuItemsForLink(link: HeaderActionLink, _classes: any, pathName: st
return (
<Menu key={link.label} trigger="hover" transitionProps={{ exitDuration: 0 }} withinPortal>
<Menu.Target>
<Link
<PreloadLink
className={classes.link}
href={link.link || ''}
data-active={pathName === link.link || undefined}
>
<>{link.label}</>
</Link>
</PreloadLink>
</Menu.Target>
{menuItems.length > 0 ? <Menu.Dropdown>{menuItems}</Menu.Dropdown> : null}
</Menu>
@@ -58,8 +58,8 @@ function getMenuItemsForLink(link: HeaderActionLink, _classes: any, pathName: st
}
export function HeaderAction({ links, navbarState, breadcrumbs }: HeaderActionProps) {
const router = useRouter();
const pathName = router.pathname;
const location = useLocation();
const pathName = location.pathname;
const [opened, { toggle }] = navbarState != null ? navbarState : [false, { toggle: () => {} }];
const { setColorScheme } = useMantineColorScheme();
@@ -71,14 +71,14 @@ export function HeaderAction({ links, navbarState, breadcrumbs }: HeaderActionPr
}
return (
<Link
<PreloadLink
key={link.label}
className={classes.link}
href={link.link || ''}
data-active={pathName === link.link || undefined}
>
{link.label}
</Link>
</PreloadLink>
);
});
return (
@@ -91,7 +91,7 @@ export function HeaderAction({ links, navbarState, breadcrumbs }: HeaderActionPr
{breadcrumbs}
</Group>
</Center>
<Group gap={5} className={classes.links} visibleFrom="sm">
<Group gap={5} visibleFrom="sm">
{items}
<ActionIcon
variant="default"

View File

@@ -1,6 +1,5 @@
import { Grid, Title } from '@mantine/core';
import { useTranslation } from 'next-i18next';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import { useTranslation } from 'react-i18next';
import ClubModal from '../components/modals/club_modal';
import ClubsTable from '../components/tables/clubs';
@@ -9,7 +8,7 @@ import { checkForAuthError, getClubs } from '../services/adapter';
import Layout from './_layout';
import classes from './index.module.css';
export default function HomePage() {
export default function ClubsPage() {
const swrClubsResponse = getClubs();
const { t } = useTranslation();
@@ -29,9 +28,3 @@ export default function HomePage() {
</Layout>
);
}
export const getServerSideProps = async ({ locale }: { locale: string }) => ({
props: {
...(await serverSideTranslations(locale, ['common'])),
},
});

View File

@@ -12,10 +12,9 @@ import {
} from '@mantine/core';
import { useForm } from '@mantine/form';
import { IconAlertCircle, IconArrowLeft } from '@tabler/icons-react';
import { useTranslation } from 'next-i18next';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import { useRouter } from 'next/router';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router';
import { PasswordStrength } from '../components/utils/password';
import { ClientOnly } from '../components/utils/react';
@@ -23,14 +22,8 @@ import { HCaptchaInput } from '../components/utils/util';
import { registerUser } from '../services/user';
import classes from './create_account.module.css';
export const getServerSideProps = async ({ locale }: { locale: string }) => ({
props: {
...(await serverSideTranslations(locale, ['common'])),
},
});
export default function CreateAccount() {
const router = useRouter();
export default function CreateAccountPage() {
const navigate = useNavigate();
const { t } = useTranslation();
const [captchaToken, setCaptchaToken] = useState<string | null>(null);
@@ -39,7 +32,7 @@ export default function CreateAccount() {
if (response != null && response.data != null && response.data.data != null) {
localStorage.setItem('login', JSON.stringify(response.data.data));
await router.push('/');
await navigate('/');
}
}
@@ -96,14 +89,14 @@ export default function CreateAccount() {
<Group justify="space-between" mt="lg" className={classes.controls}>
<ClientOnly>
<HCaptchaInput
siteKey={process.env.NEXT_PUBLIC_HCAPTCHA_SITE_KEY}
siteKey={import.meta.env.VITE_HCAPTCHA_SITE_KEY}
setCaptchaToken={setCaptchaToken}
/>
</ClientOnly>
<Anchor c="dimmed" size="sm" className={classes.control}>
<Center inline>
<IconArrowLeft size={12} stroke={1.5} />
<Box ml={5} onClick={() => router.push('/login')}>
<Box ml={5} onClick={() => navigate('/login')}>
{' '}
{t('back_to_login_nav')}
</Box>

View File

@@ -1,24 +1,17 @@
import { Alert, Button, Checkbox, Container, Paper, Title } from '@mantine/core';
import { useForm } from '@mantine/form';
import { IconAlertCircle } from '@tabler/icons-react';
import { useTranslation } from 'next-i18next';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import { useRouter } from 'next/router';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router';
import { ClientOnly } from '../components/utils/react';
import { HCaptchaInput } from '../components/utils/util';
import { registerDemoUser } from '../services/user';
import classes from './create_account.module.css';
export const getServerSideProps = async ({ locale }: { locale: string }) => ({
props: {
...(await serverSideTranslations(locale, ['common'])),
},
});
export default function CreateDemoAccount() {
const router = useRouter();
export default function CreateDemoAccountPage() {
const navigate = useNavigate();
const { t } = useTranslation();
const [captchaToken, setCaptchaToken] = useState<string | null>(null);
@@ -27,7 +20,7 @@ export default function CreateDemoAccount() {
if (response != null && response.data != null && response.data.data != null) {
localStorage.setItem('login', JSON.stringify(response.data.data));
await router.push('/');
await navigate('/');
}
}
@@ -59,7 +52,7 @@ export default function CreateDemoAccount() {
<form onSubmit={form.onSubmit(registerAndRedirect)}>
<ClientOnly>
<HCaptchaInput
siteKey={process.env.NEXT_PUBLIC_HCAPTCHA_SITE_KEY}
siteKey={import.meta.env.VITE_HCAPTCHA_SITE_KEY}
setCaptchaToken={setCaptchaToken}
/>
</ClientOnly>

View File

@@ -1,8 +1,6 @@
import { Grid, Select, Title } from '@mantine/core';
import { GetStaticProps } from 'next';
import { useTranslation } from 'next-i18next';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import TournamentsCardTable from '../components/card_tables/tournaments';
import TournamentModal from '../components/modals/tournament_modal';
@@ -48,10 +46,3 @@ export default function HomePage() {
</Layout>
);
}
type Props = {};
export const getStaticProps: GetStaticProps<Props> = async ({ locale }) => ({
props: {
...(await serverSideTranslations(locale ?? 'en', ['common'])),
},
});

View File

@@ -11,22 +11,19 @@ import {
import { useForm } from '@mantine/form';
import { showNotification } from '@mantine/notifications';
import { IconCheck } from '@tabler/icons-react';
import { useTranslation } from 'next-i18next';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import { useRouter } from 'next/router';
import React, { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router';
// import useStyles from '../components/login/login.styles';
import { tokenPresent } from '../services/local_storage';
import { performLogin } from '../services/user';
export default function Login() {
// const { classes } = useStyles();
const router = useRouter();
export default function LoginPage() {
const navigate = useNavigate();
const { t } = useTranslation();
useEffect(() => {
if (tokenPresent()) {
router.replace('/');
navigate('/');
}
}, []);
@@ -40,7 +37,7 @@ export default function Login() {
message: '',
});
await router.push('/');
await navigate('/');
}
}
@@ -110,11 +107,11 @@ export default function Login() {
</Button>
</form>
<Text c="dimmed" size="sm" ta="center" mt={15}>
<Anchor<'a'> onClick={() => router.push('/create_account')} size="sm">
<Anchor<'a'> onClick={() => navigate('/create-account')} size="sm">
{t('create_account_button')}
</Anchor>
{' - '}
<Anchor<'a'> onClick={() => router.push('/password_reset')} size="sm">
<Anchor<'a'> onClick={() => navigate('/password-reset')} size="sm">
{t('forgot_password_button')}
</Anchor>
</Text>
@@ -123,9 +120,3 @@ export default function Login() {
</>
);
}
export const getServerSideProps = async ({ locale }: { locale: string }) => ({
props: {
...(await serverSideTranslations(locale, ['common'])),
},
});

View File

@@ -21,12 +21,12 @@ import NotFoundTitle from './404';
// },
// }));
export default function ForgotPassword() {
export default function PasswordResetPage() {
// TODO: Implement this page.
return <NotFoundTitle />;
// const { classes } = useStyles();
// const router = useRouter();
// const navigate = useNavigate();
//
// return (
// <Container size={460} my={30}>

View File

@@ -1,17 +1,15 @@
import { Alert, Badge, Card, Center, Flex, Grid, Group, Stack, Text } from '@mantine/core';
import { AiOutlineHourglass } from '@react-icons/all-files/ai/AiOutlineHourglass';
import { IconAlertCircle } from '@tabler/icons-react';
import { useTranslation } from 'next-i18next';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import Head from 'next/head';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { DashboardFooter } from '../../../../components/dashboard/footer';
import { DoubleHeader, TournamentHeadTitle } from '../../../../components/dashboard/layout';
import { DoubleHeader, getTournamentHeadTitle } from '../../../../components/dashboard/layout';
import { NoContent } from '../../../../components/no_content/empty_table_info';
import { Time, compareDateTime, formatTime } from '../../../../components/utils/datetime';
import { Translator } from '../../../../components/utils/types';
import { responseIsValid } from '../../../../components/utils/util';
import { responseIsValid, setTitle } from '../../../../components/utils/util';
import { formatMatchInput1, formatMatchInput2 } from '../../../../interfaces/match';
import { getCourtsLive, getStagesLive } from '../../../../services/adapter';
import { getMatchLookup, getStageItemLookup, stringToColour } from '../../../../services/lookups';
@@ -192,8 +190,7 @@ export function Schedule({
</Group>
);
}
export default function SchedulePage() {
export default function DashboardSchedulePage() {
const { t } = useTranslation();
const tournamentResponse = getTournamentResponseByEndpointName();
@@ -201,6 +198,8 @@ export default function SchedulePage() {
const tournamentId = !notFound ? tournamentResponse[0].id : null;
const tournamentDataFull = tournamentResponse != null ? tournamentResponse[0] : null;
setTitle(getTournamentHeadTitle(tournamentDataFull));
const swrStagesResponse = getStagesLive(tournamentId);
const swrCourtsResponse = getCourtsLive(tournamentId);
@@ -211,12 +210,8 @@ export default function SchedulePage() {
if (!responseIsValid(swrStagesResponse)) return null;
if (!responseIsValid(swrCourtsResponse)) return null;
return (
<>
<Head>
<TournamentHeadTitle tournamentDataFull={tournamentDataFull} />
</Head>
<DoubleHeader tournamentData={tournamentDataFull} />
<Center>
<Group style={{ maxWidth: '48rem', width: '100%' }} px="1rem">
@@ -227,9 +222,3 @@ export default function SchedulePage() {
</>
);
}
export const getServerSideProps = async ({ locale }: { locale: string }) => ({
props: {
...(await serverSideTranslations(locale, ['common'])),
},
});

View File

@@ -1,20 +1,18 @@
import { Grid } from '@mantine/core';
import { useTranslation } from 'next-i18next';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import Head from 'next/head';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { SWRResponse } from 'swr';
import NotFoundTitle from '../../../../404';
import CourtsLarge, { CourtBadge } from '../../../../../components/brackets/courts_large';
import {
TournamentHeadTitle,
TournamentLogo,
TournamentQRCode,
TournamentTitle,
getTournamentHeadTitle,
} from '../../../../../components/dashboard/layout';
import { TableSkeletonTwoColumns } from '../../../../../components/utils/skeletons';
import { responseIsValid } from '../../../../../components/utils/util';
import { responseIsValid, setTitle } from '../../../../../components/utils/util';
import { Court } from '../../../../../interfaces/court';
import {
MatchInterface,
@@ -25,7 +23,7 @@ import { getCourtsLive, getStagesLive } from '../../../../../services/adapter';
import { getMatchLookupByCourt, getStageItemLookup } from '../../../../../services/lookups';
import { getTournamentResponseByEndpointName } from '../../../../../services/tournament';
export default function CourtsPage() {
export default function CourtsPresentPage() {
const { t } = useTranslation();
const tournamentResponse = getTournamentResponseByEndpointName();
@@ -36,6 +34,10 @@ export default function CourtsPage() {
const swrStagesResponse: SWRResponse = getStagesLive(tournamentId);
const swrCourtsResponse: SWRResponse = getCourtsLive(tournamentId);
const tournamentDataFull = tournamentResponse != null ? tournamentResponse[0] : null;
setTitle(getTournamentHeadTitle(tournamentDataFull));
if (swrStagesResponse.isLoading || swrCourtsResponse.isLoading) {
return <TableSkeletonTwoColumns />;
}
@@ -45,7 +47,6 @@ export default function CourtsPage() {
}
const stageItemsLookup = getStageItemLookup(swrStagesResponse);
const tournamentDataFull = tournamentResponse != null ? tournamentResponse[0] : null;
const courts = responseIsValid(swrCourtsResponse) ? swrCourtsResponse.data.data : [];
const matchesByCourtId = responseIsValid(swrStagesResponse)
? getMatchLookupByCourt(swrStagesResponse)
@@ -73,9 +74,6 @@ export default function CourtsPage() {
return (
<>
<Head>
<TournamentHeadTitle tournamentDataFull={tournamentDataFull} />
</Head>
<Grid style={{ margin: '1rem' }} gutter="2rem">
<Grid.Col span={{ base: 12, lg: 2 }}>
<TournamentTitle tournamentDataFull={tournamentDataFull} />
@@ -98,9 +96,3 @@ export default function CourtsPage() {
</>
);
}
export const getServerSideProps = async ({ locale }: { locale: string }) => ({
props: {
...(await serverSideTranslations(locale, ['common'])),
},
});

View File

@@ -1,23 +1,22 @@
import { Grid } from '@mantine/core';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import Head from 'next/head';
import React from 'react';
import { SWRResponse } from 'swr';
import NotFoundTitle from '../../../../404';
import {
TournamentHeadTitle,
TournamentLogo,
TournamentQRCode,
TournamentTitle,
getTournamentHeadTitle,
} from '../../../../../components/dashboard/layout';
import RequestErrorAlert from '../../../../../components/utils/error_alert';
import { TableSkeletonTwoColumns } from '../../../../../components/utils/skeletons';
import { setTitle } from '../../../../../components/utils/util';
import { getStagesLive, getTeamsLive } from '../../../../../services/adapter';
import { getTournamentResponseByEndpointName } from '../../../../../services/tournament';
import { StandingsContent } from '../standings';
export default function Standings() {
export default function StandingsPresentPage() {
const tournamentResponse = getTournamentResponseByEndpointName();
// Hack to avoid unequal number of rendered hooks.
@@ -27,6 +26,10 @@ export default function Standings() {
const swrTeamsResponse: SWRResponse = getTeamsLive(tournamentId);
const swrStagesResponse = getStagesLive(tournamentId);
const tournamentDataFull = tournamentResponse[0];
setTitle(getTournamentHeadTitle(tournamentDataFull));
if (swrTeamsResponse.isLoading) {
return <TableSkeletonTwoColumns />;
}
@@ -35,16 +38,11 @@ export default function Standings() {
return <NotFoundTitle />;
}
const tournamentDataFull = tournamentResponse[0];
if (swrTeamsResponse.error) return <RequestErrorAlert error={swrTeamsResponse.error} />;
const fontSizeInPixels = 28;
return (
<>
<Head>
<TournamentHeadTitle tournamentDataFull={tournamentDataFull} />
</Head>
<Grid style={{ margin: '1rem' }} gutter="2rem">
<Grid.Col span={{ base: 12, lg: 2 }}>
<TournamentTitle tournamentDataFull={tournamentDataFull} />
@@ -62,9 +60,3 @@ export default function Standings() {
</>
);
}
export const getServerSideProps = async ({ locale }: { locale: string }) => ({
props: {
...(await serverSideTranslations(locale, ['common'])),
},
});

View File

@@ -1,18 +1,16 @@
import { Container, Text } from '@mantine/core';
import { AiOutlineHourglass } from '@react-icons/all-files/ai/AiOutlineHourglass';
import { useTranslation } from 'next-i18next';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import Head from 'next/head';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { SWRResponse } from 'swr';
import NotFoundTitle from '../../../404';
import { DashboardFooter } from '../../../../components/dashboard/footer';
import { DoubleHeader, TournamentHeadTitle } from '../../../../components/dashboard/layout';
import { DoubleHeader, getTournamentHeadTitle } from '../../../../components/dashboard/layout';
import { NoContent } from '../../../../components/no_content/empty_table_info';
import { StandingsTableForStageItem } from '../../../../components/tables/standings';
import { TableSkeletonTwoColumns } from '../../../../components/utils/skeletons';
import { responseIsValid } from '../../../../components/utils/util';
import { responseIsValid, setTitle } from '../../../../components/utils/util';
import { getStagesLive } from '../../../../services/adapter';
import { getStageItemLookup, getStageItemTeamsLookup } from '../../../../services/lookups';
import { getTournamentResponseByEndpointName } from '../../../../services/tournament';
@@ -65,7 +63,7 @@ export function StandingsContent({
return rows;
}
export default function Standings() {
export default function DashboardStandingsPage() {
const tournamentResponse = getTournamentResponseByEndpointName();
const tournamentDataFull = tournamentResponse ? tournamentResponse[0] : null;
@@ -75,6 +73,8 @@ export default function Standings() {
const swrStagesResponse = getStagesLive(tournamentId);
setTitle(getTournamentHeadTitle(tournamentDataFull));
if (!tournamentResponse) {
return <TableSkeletonTwoColumns />;
}
@@ -89,9 +89,6 @@ export default function Standings() {
return (
<>
<Head>
<TournamentHeadTitle tournamentDataFull={tournamentDataFull} />
</Head>
<DoubleHeader tournamentData={tournamentDataFull} />
<Container mt="1rem" px="0rem">
<Container style={{ width: '100%' }} px="sm">
@@ -106,9 +103,3 @@ export default function Standings() {
</>
);
}
export const getServerSideProps = async ({ locale }: { locale: string }) => ({
props: {
...(await serverSideTranslations(locale, ['common'])),
},
});

View File

@@ -1,7 +1,6 @@
import { Grid, Title } from '@mantine/core';
import { useTranslation } from 'next-i18next';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import React from 'react';
import { useTranslation } from 'react-i18next';
import PlayerCreateModal from '../../../components/modals/player_create_modal';
import PlayersTable from '../../../components/tables/players';
@@ -10,7 +9,7 @@ import { capitalize, getTournamentIdFromRouter } from '../../../components/utils
import { getPlayersPaginated } from '../../../services/adapter';
import TournamentLayout from '../_tournament_layout';
export default function Players() {
export default function PlayersPage() {
const tableState = getTableState('name');
const { tournamentData } = getTournamentIdFromRouter();
const swrPlayersResponse = getPlayersPaginated(
@@ -41,9 +40,3 @@ export default function Players() {
</TournamentLayout>
);
}
export const getServerSideProps = async ({ locale }: { locale: string }) => ({
props: {
...(await serverSideTranslations(locale, ['common'])),
},
});

View File

@@ -1,8 +1,7 @@
import { Accordion, Badge, Button, Center, Checkbox, Container, NumberInput } from '@mantine/core';
import { useForm } from '@mantine/form';
import { useTranslation } from 'next-i18next';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { SWRResponse } from 'swr';
import DeleteButton from '../../../components/buttons/delete';
@@ -201,9 +200,3 @@ export default function RankingsPage() {
</TournamentLayout>
);
}
export const getServerSideProps = async ({ locale }: { locale: string }) => ({
props: {
...(await serverSideTranslations(locale, ['common'])),
},
});

View File

@@ -13,9 +13,8 @@ import {
} from '@mantine/core';
import { AiOutlineHourglass } from '@react-icons/all-files/ai/AiOutlineHourglass';
import { IconAlertCircle } from '@tabler/icons-react';
import { useTranslation } from 'next-i18next';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import MatchModal from '../../../components/modals/match_modal';
import { NoContent } from '../../../components/no_content/empty_table_info';
@@ -224,7 +223,7 @@ function Schedule({
);
}
export default function SchedulePage() {
export default function ResultsPage() {
const [modalOpened, modalSetOpened] = useState(false);
const [match, setMatch] = useState<MatchInterface | null>(null);
@@ -276,9 +275,3 @@ export default function SchedulePage() {
</TournamentLayout>
);
}
export const getServerSideProps = async ({ locale }: { locale: string }) => ({
props: {
...(await serverSideTranslations(locale, ['common'])),
},
});

View File

@@ -14,9 +14,8 @@ import {
} from '@mantine/core';
import { AiFillWarning } from '@react-icons/all-files/ai/AiFillWarning';
import { IconAlertCircle, IconCalendarPlus, IconDots, IconTrash } from '@tabler/icons-react';
import { useTranslation } from 'next-i18next';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { SWRResponse } from 'swr';
import CourtModal from '../../../components/modals/create_court_modal';
@@ -340,9 +339,3 @@ export default function SchedulePage() {
</TournamentLayout>
);
}
export const getServerSideProps = async ({ locale }: { locale: string }) => ({
props: {
...(await serverSideTranslations(locale, ['common'])),
},
});

View File

@@ -19,15 +19,14 @@ import { useForm } from '@mantine/form';
import { MdDelete } from '@react-icons/all-files/md/MdDelete';
import { MdUnarchive } from '@react-icons/all-files/md/MdUnarchive';
import { IconCalendar, IconCalendarTime, IconCopy, IconPencil } from '@tabler/icons-react';
import assert from 'assert';
import { useTranslation } from 'next-i18next';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import { useRouter } from 'next/router';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { MdArchive } from 'react-icons/md';
import { useNavigate } from 'react-router';
import { SWRResponse } from 'swr';
import NotFoundTitle from '../../404';
import { assert_not_none } from '../../../components/utils/assert';
import { DropzoneButton } from '../../../components/utils/file_upload';
import { GenericSkeletonThreeRows } from '../../../components/utils/skeletons';
import { capitalize, getBaseURL, getTournamentIdFromRouter } from '../../../components/utils/util';
@@ -124,7 +123,7 @@ function GeneralTournamentForm({
swrTournamentResponse: SWRResponse;
clubs: Club[];
}) {
const router = useRouter();
const navigate = useNavigate();
const { t } = useTranslation();
const form = useForm({
@@ -154,7 +153,7 @@ function GeneralTournamentForm({
return (
<form
onSubmit={form.onSubmit(async (values) => {
assert(values.club_id != null);
assert_not_none(values.club_id);
await updateTournament(
tournament.id,
@@ -320,7 +319,7 @@ function GeneralTournamentForm({
onClick={async () => {
await deleteTournament(tournament.id)
.then(async () => {
await router.push('/');
await navigate('/');
})
.catch((response: any) => handleRequestError(response));
}}
@@ -377,9 +376,3 @@ export default function SettingsPage() {
</TournamentLayout>
);
}
export const getServerSideProps = async ({ locale }: { locale: string }) => ({
props: {
...(await serverSideTranslations(locale, ['common'])),
},
});

View File

@@ -1,7 +1,6 @@
import { Group, Stack } from '@mantine/core';
import { useTranslation } from 'next-i18next';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import React from 'react';
import { useTranslation } from 'react-i18next';
import Builder from '../../../../components/builder/builder';
import { CreateStageButtonLarge } from '../../../../components/buttons/create_stage';
@@ -84,9 +83,3 @@ export default function StagesPage() {
return <TournamentLayout tournament_id={tournamentData.id}>{content}</TournamentLayout>;
}
export const getServerSideProps = async ({ locale }: { locale: string }) => ({
props: {
...(await serverSideTranslations(locale, ['common'])),
},
});

View File

@@ -1,9 +1,8 @@
import { Button, Container, Grid, Group, SegmentedControl, Stack, Title } from '@mantine/core';
import { IconExternalLink } from '@tabler/icons-react';
import { useTranslation } from 'next-i18next';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import Link from 'next/link';
import { parseAsBoolean, parseAsInteger, parseAsString, useQueryState, useQueryStates } from 'nuqs';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { LuNavigation } from 'react-icons/lu';
import { SWRResponse } from 'swr';
@@ -12,7 +11,7 @@ import { RoundsGridCols } from '../../../../../components/brackets/brackets';
import { NoContent } from '../../../../../components/no_content/empty_table_info';
import Scheduler from '../../../../../components/scheduling/scheduling';
import classes from '../../../../../components/utility.module.css';
import { useRouterQueryState } from '../../../../../components/utils/query_parameters';
import PreloadLink from '../../../../../components/utils/link';
import { Translator } from '../../../../../components/utils/types';
import {
getStageItemIdFromRouter,
@@ -34,12 +33,6 @@ import {
import { getStageItemLookup } from '../../../../../services/lookups';
import TournamentLayout from '../../../_tournament_layout';
export const getServerSideProps = async ({ locale }: { locale: string }) => ({
props: {
...(await serverSideTranslations(locale, ['common'])),
},
});
function NoCourtsButton({ t, tournamentData }: { t: Translator; tournamentData: Tournament }) {
return (
<Stack align="center">
@@ -49,7 +42,7 @@ function NoCourtsButton({ t, tournamentData }: { t: Translator; tournamentData:
size="lg"
leftSection={<LuNavigation size={24} />}
variant="outline"
component={Link}
component={PreloadLink}
className={classes.mobileLink}
href={`/tournaments/${tournamentData.id}/schedule`}
>
@@ -59,7 +52,7 @@ function NoCourtsButton({ t, tournamentData }: { t: Translator; tournamentData:
);
}
export default function TournamentPage() {
export default function SwissTournamentPage() {
const { id, tournamentData } = getTournamentIdFromRouter();
const stageItemId = getStageItemIdFromRouter();
const { t } = useTranslation();
@@ -68,15 +61,31 @@ export default function TournamentPage() {
checkForAuthError(swrTournamentResponse);
const swrStagesResponse: SWRResponse = getStages(id);
const swrCourtsResponse = getCourts(tournamentData.id);
const [onlyRecommended, setOnlyRecommended] = useRouterQueryState('only-recommended', 'true');
const [eloThreshold, setEloThreshold] = useRouterQueryState('max-elo-diff', 200);
const [iterations, setIterations] = useRouterQueryState('iterations', 2_000);
const [limit, setLimit] = useRouterQueryState('limit', 50);
const [matchVisibility, setMatchVisibility] = useRouterQueryState('match-visibility', 'all');
const [teamNamesDisplay, setTeamNamesDisplay] = useRouterQueryState('which-names', 'team-names');
const [showAdvancedSchedulingOptions, setShowAdvancedSchedulingOptions] = useRouterQueryState(
const [onlyRecommended, setOnlyRecommended] = useQueryState(
'only-recommended',
parseAsString.withDefault('true')
);
const [eloThreshold, setEloThreshold] = useQueryState(
'max-elo-diff',
parseAsInteger.withDefault(200)
);
const [iterations, setIterations] = useQueryState(
'iterations',
parseAsInteger.withDefault(2_000)
);
const [limit, setLimit] = useQueryState('limit', parseAsInteger.withDefault(50));
const [matchVisibility, setMatchVisibility] = useQueryState(
'match-visibility',
parseAsString.withDefault('all')
);
const [teamNamesDisplay, setTeamNamesDisplay] = useQueryState(
'which-names',
parseAsString.withDefault('team-names')
);
const [showAdvancedSchedulingOptions, setShowAdvancedSchedulingOptions] = useQueryState(
'advanced',
'false'
parseAsString.withDefault('false')
);
const displaySettings: BracketDisplaySettings = {
matchVisibility,

View File

@@ -1,7 +1,6 @@
import { Grid, Select, Title } from '@mantine/core';
import { useTranslation } from 'next-i18next';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import TeamCreateModal from '../../../components/modals/team_create_modal';
import { getTableState, tableStateToPagination } from '../../../components/tables/table';
@@ -43,7 +42,7 @@ function StageItemSelect({
);
}
export default function Teams() {
export default function TeamsPage() {
const tableState = getTableState('name');
const { t } = useTranslation();
const [filteredStageItemId, setFilteredStageItemId] = useState(null);
@@ -100,9 +99,3 @@ export default function Teams() {
</TournamentLayout>
);
}
export const getServerSideProps = async ({ locale }: { locale: string }) => ({
props: {
...(await serverSideTranslations(locale, ['common'])),
},
});

View File

@@ -1,6 +1,6 @@
import { Group, ThemeIcon, Title, Tooltip } from '@mantine/core';
import { useTranslation } from 'next-i18next';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { HiArchiveBoxArrowDown } from 'react-icons/hi2';
import { TournamentLinks } from '../../components/navbar/_main_links';

View File

@@ -1,19 +1,12 @@
import { Group, Stack, Title } from '@mantine/core';
import { useTranslation } from 'next-i18next';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import { useTranslation } from 'react-i18next';
import UserForm from '../components/forms/user';
import { TableSkeletonSingleColumn } from '../components/utils/skeletons';
import { checkForAuthError, getUser } from '../services/adapter';
import Layout from './_layout';
export const getServerSideProps = async ({ locale }: { locale: string }) => ({
props: {
...(await serverSideTranslations(locale, ['common'])),
},
});
export default function HomePage() {
export default function UserPage() {
const { t, i18n } = useTranslation();
const swrUserResponse = getUser();

View File

@@ -1,7 +1,6 @@
import { showNotification } from '@mantine/notifications';
import type Axios from 'axios';
import { AxiosError, AxiosInstance, AxiosResponse } from 'axios';
import { useRouter } from 'next/router';
import axios, { AxiosError, AxiosInstance, AxiosResponse } from 'axios';
import { useNavigate } from 'react-router';
import useSWR, { SWRResponse } from 'swr';
import { Pagination } from '../components/utils/util';
@@ -10,9 +9,6 @@ import { RoundInterface } from '../interfaces/round';
import { TournamentFilter } from '../interfaces/tournament';
import { getLogin, performLogout, tokenPresent } from './local_storage';
// TODO: This is a workaround for the fact that axios is not properly typed.
const axios: typeof Axios = require('axios').default;
export function handleRequestError(response: AxiosError) {
if (response.code === 'ERR_NETWORK') {
showNotification({
@@ -53,8 +49,8 @@ export function requestSucceeded(result: AxiosResponse | AxiosError) {
}
export function getBaseApiUrl() {
return process.env.NEXT_PUBLIC_API_BASE_URL != null
? process.env.NEXT_PUBLIC_API_BASE_URL
return import.meta.env.VITE_API_BASE_URL != null
? import.meta.env.VITE_API_BASE_URL
: 'http://localhost:8400';
}
@@ -239,8 +235,8 @@ export async function removeTeamLogo(tournament_id: number, team_id: number) {
export function checkForAuthError(response: any) {
if (typeof window !== 'undefined' && !tokenPresent()) {
const router = useRouter();
router.push('/login');
const navigate = useNavigate();
navigate('/login');
}
// We send a simple GET `/clubs` request to test whether we really should log out. // Next

View File

@@ -1,7 +1,7 @@
import { showNotification } from '@mantine/notifications';
import { IconCheck } from '@tabler/icons-react';
import { NextRouter } from 'next/router';
import React from 'react';
import { NavigateFunction } from 'react-router';
import { Translator } from '../components/utils/types';
@@ -9,7 +9,7 @@ export function performLogout() {
localStorage.removeItem('login');
}
export function performLogoutAndRedirect(t: Translator, router: NextRouter) {
export function performLogoutAndRedirect(t: Translator, navigate: NavigateFunction) {
performLogout();
showNotification({
@@ -19,7 +19,7 @@ export function performLogoutAndRedirect(t: Translator, router: NextRouter) {
message: '',
autoClose: 10000,
});
router.push('/login');
navigate('/login');
}
export function getLogin() {

View File

@@ -1,6 +1,6 @@
import assert from 'assert';
import { SWRResponse } from 'swr';
import { assert_not_none } from '../components/utils/assert';
import { groupBy, responseIsValid } from '../components/utils/util';
import { Court } from '../interfaces/court';
import { MatchInterface } from '../interfaces/match';
@@ -120,9 +120,9 @@ export function getScheduleData(
matches: (matchesByCourtId[court.id] || [])
.filter((match: MatchInterface) => match.start_time != null)
.sort((m1: MatchInterface, m2: MatchInterface) => {
assert(m1.position_in_schedule != null);
assert(m2.position_in_schedule != null);
return m1.position_in_schedule > m2.position_in_schedule ? 1 : -1 || [];
return assert_not_none(m1.position_in_schedule) > assert_not_none(m2.position_in_schedule)
? 1
: -1 || [];
}),
court,
}));

View File

@@ -15,6 +15,6 @@
"jsx": "preserve",
"incremental": true
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"include": [ "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}

1
frontend/types.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
declare module '*.module.css';

Some files were not shown because too many files have changed in this diff Show More