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
13
.github/workflows/frontend.yml
vendored
@@ -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
@@ -6,6 +6,7 @@ uv.lock
|
||||
.mypy_cache
|
||||
.dmypy.json
|
||||
.pytest_cache
|
||||
.yarn
|
||||
|
||||
*.env
|
||||
!ci.env
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -56,7 +56,7 @@ two sections.
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
yarn run dev
|
||||
pnpm run dev
|
||||
```
|
||||
|
||||
### Backend
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`.
|
||||
|
||||
|
||||
@@ -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
@@ -14,6 +14,7 @@
|
||||
|
||||
# production
|
||||
/build
|
||||
/dist
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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
@@ -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
@@ -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>
|
||||
@@ -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);
|
||||
@@ -1 +0,0 @@
|
||||
import '@testing-library/jest-dom/extend-expect';
|
||||
5
frontend/next-env.d.ts
vendored
@@ -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.
|
||||
@@ -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')
|
||||
};
|
||||
@@ -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,
|
||||
});
|
||||
@@ -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
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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';
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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')}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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" />,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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' }}
|
||||
|
||||
@@ -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 = [
|
||||
{
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
6
frontend/src/components/utils/assert.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
export function assert_not_none<T>(value: T | null) {
|
||||
if (value === null) {
|
||||
throw new Error('Assertion failed');
|
||||
}
|
||||
return value;
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
12
frontend/src/components/utils/link.tsx
Normal 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;
|
||||
@@ -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 (
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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
@@ -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();
|
||||
@@ -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'])),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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'])),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'])),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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'])),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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'])),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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'])),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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'])),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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'])),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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'])),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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'])),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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'])),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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'])),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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'])),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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'])),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'])),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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,
|
||||
}));
|
||||
|
||||
@@ -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
@@ -0,0 +1 @@
|
||||
declare module '*.module.css';
|
||||