diff --git a/.github/workflows/deploy.next.yml b/.github/workflows/deploy.next.yml index 6e5a11d..eb7ea47 100644 --- a/.github/workflows/deploy.next.yml +++ b/.github/workflows/deploy.next.yml @@ -27,7 +27,7 @@ jobs: - name: Fetch OpenAPI schema run: npm run open:api - name: Generate API client - run: npm run open:generate + run: npm run openapi-ts - name: Build and deploy to Next environment run: docker-compose -f docker-compose.next.yml up -d --build env: diff --git a/.github/workflows/deploy.production.yml b/.github/workflows/deploy.production.yml index 6917beb..96aebe0 100644 --- a/.github/workflows/deploy.production.yml +++ b/.github/workflows/deploy.production.yml @@ -19,7 +19,7 @@ jobs: - name: Fetch OpenAPI schema run: npm run open:api - name: Generate API client - run: npm run open:generate + run: npm run openapi-ts - name: Build and deploy to Next environment run: docker-compose -f docker-compose.yml up -d --build env: diff --git a/.gitignore b/.gitignore index 27ff020..cd93a43 100644 --- a/.gitignore +++ b/.gitignore @@ -37,4 +37,6 @@ next-env.d.ts openapi.json /generated .idea -.env \ No newline at end of file +.env + +/client \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 30bab2a..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - \ No newline at end of file diff --git a/openapi-ts.config.ts b/openapi-ts.config.ts new file mode 100644 index 0000000..4730b46 --- /dev/null +++ b/openapi-ts.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from "@hey-api/openapi-ts"; + +export default defineConfig({ + client: "@hey-api/client-fetch", + input: "./openapi.json", + output: { + format: "prettier", + path: "./client", + }, + types: { + enums: "typescript", + }, +}); diff --git a/package-lock.json b/package-lock.json index 416d8a7..2ff4864 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "aimcup-frontend", "version": "0.1.0", "dependencies": { + "@hey-api/client-fetch": "^0.2.4", "@next/font": "^14.2.4", "@reduxjs/toolkit": "^2.2.5", "@types/node": "20.6.0", @@ -34,6 +35,7 @@ "zod": "^3.23.8" }, "devDependencies": { + "@hey-api/openapi-ts": "^0.52.8", "@typescript-eslint/eslint-plugin": "^6.14.0", "@typescript-eslint/parser": "^6.14.0", "autoprefixer": "^10.4.16", @@ -43,7 +45,6 @@ "eslint-plugin-import": "^2.29.1", "husky": "^8.0.3", "lint-staged": "^15.2.0", - "openapi-typescript-codegen": "^0.29.0", "postcss": "^8.4.32", "prettier": "^3.1.1", "prettier-plugin-tailwindcss": "^0.5.9", @@ -71,10 +72,11 @@ } }, "node_modules/@apidevtools/json-schema-ref-parser": { - "version": "11.6.2", - "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.6.2.tgz", - "integrity": "sha512-ENUdLLT04aDbbHCRwfKf8gR67AhV0CdFrOAtk+FcakBAgaq6ds3HLK9X0BCyiFUz8pK9uP+k6YZyJaGG7Mt7vQ==", + "version": "11.7.0", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.7.0.tgz", + "integrity": "sha512-pRrmXMCwnmrkS3MLgAIW5dXRzeTv6GLjkjb4HmxNnvAKXN1Nfzp4KmGADBQvlVUcqi+a5D+hfGDLLnd5NnYxog==", "dev": true, + "license": "MIT", "dependencies": { "@jsdevtools/ono": "^7.1.3", "@types/json-schema": "^7.0.15", @@ -212,6 +214,44 @@ "integrity": "sha512-sTcG+QZ6fdEUObICavU+aB3Mp8HY4n14wYHdxK4fXjPmv3PXZZeY5RaguJmGyeH/CJQhX3fqKUtS4qc1LoHwhQ==", "license": "MIT" }, + "node_modules/@hey-api/client-fetch": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@hey-api/client-fetch/-/client-fetch-0.2.4.tgz", + "integrity": "sha512-SGTVAVw3PlKDLw+IyhNhb/jCH3P1P2xJzLxA8Kyz1g95HrkYOJdRpl9F5I7LLwo9aCIB7nwR2NrSeX7QaQD7vQ==", + "license": "MIT" + }, + "node_modules/@hey-api/openapi-ts": { + "version": "0.52.8", + "resolved": "https://registry.npmjs.org/@hey-api/openapi-ts/-/openapi-ts-0.52.8.tgz", + "integrity": "sha512-OKSHz3083Qnxn+RyPwRzCFjmeN3ZPmSw8aEWwsKyeVU1vik4H1MBfuFDiIP7luf929AYEN3nLA2mrr9wVRENnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@apidevtools/json-schema-ref-parser": "11.7.0", + "c12": "1.11.1", + "commander": "12.1.0", + "handlebars": "4.7.8" + }, + "bin": { + "openapi-ts": "bin/index.cjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "typescript": "^5.x" + } + }, + "node_modules/@hey-api/openapi-ts/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.11", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz", @@ -815,7 +855,8 @@ "version": "7.1.3", "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@next/env": { "version": "14.2.4", @@ -1214,6 +1255,7 @@ "version": "6.14.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.14.0.tgz", "integrity": "sha512-QjToC14CKacd4Pa7JK4GeB/vHmWFJckec49FR4hmIRf97+KXole0T97xxu9IFiPxVQ1DBWrQ5wreLwAGwWAVQA==", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "6.14.0", "@typescript-eslint/types": "6.14.0", @@ -1360,9 +1402,11 @@ } }, "node_modules/acorn": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", - "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1732,6 +1776,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001565", "electron-to-chromium": "^1.4.601", @@ -1756,6 +1801,35 @@ "node": ">=10.16.0" } }, + "node_modules/c12": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/c12/-/c12-1.11.1.tgz", + "integrity": "sha512-KDU0TvSvVdaYcQKQ6iPHATGz/7p/KiVjPg4vQrB6Jg/wX9R0yl5RZxWm9IoZqaIHD2+6PZd81+KMGwRr/lRIUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.6.0", + "confbox": "^0.1.7", + "defu": "^6.1.4", + "dotenv": "^16.4.5", + "giget": "^1.2.3", + "jiti": "^1.21.6", + "mlly": "^1.7.1", + "ohash": "^1.1.3", + "pathe": "^1.1.2", + "perfect-debounce": "^1.0.0", + "pkg-types": "^1.1.1", + "rc9": "^2.1.2" + }, + "peerDependencies": { + "magicast": "^0.3.4" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } + } + }, "node_modules/call-bind": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", @@ -1783,18 +1857,6 @@ "node": ">=6" } }, - "node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/camelcase-css": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", @@ -1840,16 +1902,11 @@ } }, "node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], + "license": "MIT", "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -1862,6 +1919,9 @@ "engines": { "node": ">= 8.10.0" }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, "optionalDependencies": { "fsevents": "~2.3.2" } @@ -1878,6 +1938,26 @@ "node": ">= 6" } }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/citty": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "consola": "^3.2.3" + } + }, "node_modules/cli-cursor": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", @@ -1990,6 +2070,23 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, + "node_modules/confbox": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.7.tgz", + "integrity": "sha512-uJcB/FKZtBMCJpK8MQji6bJHgu1tixKPxRLeGkNzBoOZzpnZUJm0jm2/sBDWcuBx1dYgxV4JU+g5hmNxCyAmdA==", + "dev": true, + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.2.3.tgz", + "integrity": "sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, "node_modules/cross-env": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", @@ -2166,6 +2263,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/defu": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "dev": true, + "license": "MIT" + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -2174,6 +2278,13 @@ "node": ">=6" } }, + "node_modules/destr": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.3.tgz", + "integrity": "sha512-2N3BOUU4gYMpTP24s5rF5iP7BDr7uNTCs4ozw3kf/eKfvWSIu93GEBi5m427YoyJoeOzQ5smuu4nNAPGb8idSQ==", + "dev": true, + "license": "MIT" + }, "node_modules/detect-libc": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", @@ -2216,6 +2327,19 @@ "node": ">=6.0.0" } }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -2231,7 +2355,8 @@ "node_modules/embla-carousel": { "version": "8.1.3", "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.1.3.tgz", - "integrity": "sha512-GiRpKtzidV3v50oVMly8S+D7iE1r96ttt7fSlvtyKHoSkzrAnVcu8fX3c4j8Ol2hZSQlVfDqDIqdrFPs0u5TWQ==" + "integrity": "sha512-GiRpKtzidV3v50oVMly8S+D7iE1r96ttt7fSlvtyKHoSkzrAnVcu8fX3c4j8Ol2hZSQlVfDqDIqdrFPs0u5TWQ==", + "peer": true }, "node_modules/embla-carousel-react": { "version": "8.1.3", @@ -2425,6 +2550,7 @@ "version": "8.49.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.49.0.tgz", "integrity": "sha512-jw03ENfm6VJI0jA9U+8H5zfl5b+FvuU3YYvZRdZHOlU2ggJkxrlkJH4HcDrZpj6YwD8kuYqvQM8LyesoazrSOQ==", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -2582,6 +2708,7 @@ "version": "2.29.1", "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz", "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==", + "peer": true, "dependencies": { "array-includes": "^3.1.7", "array.prototype.findlastindex": "^1.2.3", @@ -3015,18 +3142,30 @@ "url": "https://github.com/sponsors/rawify" } }, - "node_modules/fs-extra": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", - "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", "dev": true, + "license": "ISC", "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" + "yallist": "^4.0.0" }, "engines": { - "node": ">=14.14" + "node": ">=8" } }, "node_modules/fs.realpath": { @@ -3150,6 +3289,26 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/giget": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/giget/-/giget-1.2.3.tgz", + "integrity": "sha512-8EHPljDvs7qKykr6uw8b+lqLiUc/vUg+KVTI0uND4s63TdsZM2Xus3mflvF0DDG9SiM4RlCkFGL+7aAjRmV7KA==", + "dev": true, + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.2.3", + "defu": "^6.1.4", + "node-fetch-native": "^1.6.3", + "nypm": "^0.3.8", + "ohash": "^1.1.3", + "pathe": "^1.1.2", + "tar": "^6.2.0" + }, + "bin": { + "giget": "dist/cli.mjs" + } + }, "node_modules/glob": { "version": "7.1.7", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", @@ -3253,6 +3412,7 @@ "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", "dev": true, + "license": "MIT", "dependencies": { "minimist": "^1.2.5", "neo-async": "^2.6.2", @@ -3832,10 +3992,11 @@ } }, "node_modules/jiti": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz", - "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==", + "version": "1.21.6", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz", + "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==", "dev": true, + "license": "MIT", "bin": { "jiti": "bin/jiti.js" } @@ -3891,18 +4052,6 @@ "json5": "lib/cli.js" } }, - "node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, "node_modules/jsx-ast-utils": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", @@ -4239,6 +4388,59 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mlly": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.1.tgz", + "integrity": "sha512-rrVRZRELyQzrIUAVMHxP97kv+G786pHmOKzuFII8zDYahFBS7qnHh2AlYSl1GAHhaMPCz6/oHjVMcfFYgFYHgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.3", + "pathe": "^1.1.2", + "pkg-types": "^1.1.1", + "ufo": "^1.5.3" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -4281,13 +4483,15 @@ "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/next": { "version": "14.2.4", "resolved": "https://registry.npmjs.org/next/-/next-14.2.4.tgz", "integrity": "sha512-R8/V7vugY+822rsQGQCjoLhMuC9oFj9SOi4Cl4b2wjDrseD0LRZ10W7R6Czo4w9ZznVSshKjuIomsRjvm9EKJQ==", "license": "MIT", + "peer": true, "dependencies": { "@next/env": "14.2.4", "@swc/helpers": "0.5.5", @@ -4377,6 +4581,13 @@ "react-dom": ">= 16.0.0" } }, + "node_modules/node-fetch-native": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.4.tgz", + "integrity": "sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==", + "dev": true, + "license": "MIT" + }, "node_modules/node-releases": { "version": "2.0.14", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", @@ -4433,6 +4644,27 @@ "resolved": "https://registry.npmjs.org/nprogress/-/nprogress-0.2.0.tgz", "integrity": "sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==" }, + "node_modules/nypm": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.3.9.tgz", + "integrity": "sha512-BI2SdqqTHg2d4wJh8P9A1W+bslg33vOE9IZDY6eR2QC+Pu1iNBVZUqczrd43rJb+fMzHU7ltAYKsEFY/kHMFcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.2.3", + "execa": "^8.0.1", + "pathe": "^1.1.2", + "pkg-types": "^1.1.1", + "ufo": "^1.5.3" + }, + "bin": { + "nypm": "dist/cli.mjs" + }, + "engines": { + "node": "^14.16.0 || >=16.10.0" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -4567,6 +4799,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/ohash": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-1.1.3.tgz", + "integrity": "sha512-zuHHiGTYTA1sYJ/wZN+t5HKZaH23i4yI1HMwbuXm24Nid7Dv0KcuRlKoNKS9UNfAVSBlnGLcuQrnOKWOZoEGaw==", + "dev": true, + "license": "MIT" + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -4590,31 +4829,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/openapi-typescript-codegen": { - "version": "0.29.0", - "resolved": "https://registry.npmjs.org/openapi-typescript-codegen/-/openapi-typescript-codegen-0.29.0.tgz", - "integrity": "sha512-/wC42PkD0LGjDTEULa/XiWQbv4E9NwLjwLjsaJ/62yOsoYhwvmBR31kPttn1DzQ2OlGe5stACcF/EIkZk43M6w==", - "dev": true, - "dependencies": { - "@apidevtools/json-schema-ref-parser": "^11.5.4", - "camelcase": "^6.3.0", - "commander": "^12.0.0", - "fs-extra": "^11.2.0", - "handlebars": "^4.7.8" - }, - "bin": { - "openapi": "bin/index.js" - } - }, - "node_modules/openapi-typescript-codegen/node_modules/commander": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", - "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", - "dev": true, - "engines": { - "node": ">=18" - } - }, "node_modules/optionator": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", @@ -4729,6 +4943,20 @@ "node": ">=8" } }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -4775,6 +5003,18 @@ "node": ">= 6" } }, + "node_modules/pkg-types": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.1.3.tgz", + "integrity": "sha512-+JrgthZG6m3ckicaOB74TwQ+tBWsFl3qVQg7mN8ulwSOElJ7gBhKzj2VkCPnZ4NlF6kEquYU+RIYNVAvzd54UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.7", + "mlly": "^1.7.1", + "pathe": "^1.1.2" + } + }, "node_modules/postcss": { "version": "8.4.32", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.32.tgz", @@ -4794,6 +5034,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.0.0", @@ -4934,6 +5175,7 @@ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.1.tgz", "integrity": "sha512-22UbSzg8luF4UuZtzgiUOfcGM8s4tjBv6dJRT7j275NXsy2jb4aJa4NNveul5x4eqlF1wuhuR2RElK71RvmVaw==", "dev": true, + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -5084,11 +5326,23 @@ "integrity": "sha512-jLN68Dx5kyFHaePoXWPsCGW5qdyZQtLYHkxkg02/Mz6g0kYpDx4FyP6XfArhQdlOC4b8Mv+EMxPo/8La7Tzghg==", "license": "MIT" }, + "node_modules/rc9": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", + "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defu": "^6.1.4", + "destr": "^2.0.3" + } + }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -5118,6 +5372,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -5172,6 +5427,7 @@ "version": "9.1.2", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.1.2.tgz", "integrity": "sha512-0OA4dhM1W48l3uzmv6B7TXPCGmokUU4p1M44DGN2/D9a1FjVPukVjER1PcPX97jIg6aUeLq1XJo1IpfbgULn0w==", + "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.3", "use-sync-external-store": "^1.0.0" @@ -5223,7 +5479,8 @@ "node_modules/redux": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", - "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==" + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "peer": true }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -5636,6 +5893,7 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -6009,6 +6267,34 @@ "node": ">=6" } }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dev": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -6166,6 +6452,7 @@ "version": "5.2.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -6174,11 +6461,19 @@ "node": ">=14.17" } }, + "node_modules/ufo": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.4.tgz", + "integrity": "sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==", + "dev": true, + "license": "MIT" + }, "node_modules/uglify-js": { - "version": "3.17.4", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", - "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==", + "version": "3.19.2", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.2.tgz", + "integrity": "sha512-S8KA6DDI47nQXJSi2ctQ629YzwOVs+bQML6DAtvy0wgNdpi+0ySpQK0g2pxBq2xfF2z3YCscu7NNA8nXT9PlIQ==", "dev": true, + "license": "BSD-2-Clause", "optional": true, "bin": { "uglifyjs": "bin/uglifyjs" @@ -6201,15 +6496,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "engines": { - "node": ">= 10.0.0" - } - }, "node_modules/update-browserslist-db": { "version": "1.0.13", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", @@ -6344,7 +6630,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/wrap-ansi": { "version": "9.0.0", @@ -6454,6 +6741,13 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, "node_modules/yaml": { "version": "2.3.4", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", @@ -6496,9 +6790,9 @@ "dev": true }, "@apidevtools/json-schema-ref-parser": { - "version": "11.6.2", - "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.6.2.tgz", - "integrity": "sha512-ENUdLLT04aDbbHCRwfKf8gR67AhV0CdFrOAtk+FcakBAgaq6ds3HLK9X0BCyiFUz8pK9uP+k6YZyJaGG7Mt7vQ==", + "version": "11.7.0", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.7.0.tgz", + "integrity": "sha512-pRrmXMCwnmrkS3MLgAIW5dXRzeTv6GLjkjb4HmxNnvAKXN1Nfzp4KmGADBQvlVUcqi+a5D+hfGDLLnd5NnYxog==", "dev": true, "requires": { "@jsdevtools/ono": "^7.1.3", @@ -6597,6 +6891,31 @@ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.5.tgz", "integrity": "sha512-sTcG+QZ6fdEUObICavU+aB3Mp8HY4n14wYHdxK4fXjPmv3PXZZeY5RaguJmGyeH/CJQhX3fqKUtS4qc1LoHwhQ==" }, + "@hey-api/client-fetch": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@hey-api/client-fetch/-/client-fetch-0.2.4.tgz", + "integrity": "sha512-SGTVAVw3PlKDLw+IyhNhb/jCH3P1P2xJzLxA8Kyz1g95HrkYOJdRpl9F5I7LLwo9aCIB7nwR2NrSeX7QaQD7vQ==" + }, + "@hey-api/openapi-ts": { + "version": "0.52.8", + "resolved": "https://registry.npmjs.org/@hey-api/openapi-ts/-/openapi-ts-0.52.8.tgz", + "integrity": "sha512-OKSHz3083Qnxn+RyPwRzCFjmeN3ZPmSw8aEWwsKyeVU1vik4H1MBfuFDiIP7luf929AYEN3nLA2mrr9wVRENnw==", + "dev": true, + "requires": { + "@apidevtools/json-schema-ref-parser": "11.7.0", + "c12": "1.11.1", + "commander": "12.1.0", + "handlebars": "4.7.8" + }, + "dependencies": { + "commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true + } + } + }, "@humanwhocodes/config-array": { "version": "0.11.11", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz", @@ -7098,6 +7417,7 @@ "version": "6.14.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.14.0.tgz", "integrity": "sha512-QjToC14CKacd4Pa7JK4GeB/vHmWFJckec49FR4hmIRf97+KXole0T97xxu9IFiPxVQ1DBWrQ5wreLwAGwWAVQA==", + "peer": true, "requires": { "@typescript-eslint/scope-manager": "6.14.0", "@typescript-eslint/types": "6.14.0", @@ -7171,9 +7491,10 @@ } }, "acorn": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", - "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==" + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "peer": true }, "acorn-jsx": { "version": "5.3.2", @@ -7421,6 +7742,7 @@ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.2.tgz", "integrity": "sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==", "dev": true, + "peer": true, "requires": { "caniuse-lite": "^1.0.30001565", "electron-to-chromium": "^1.4.601", @@ -7436,6 +7758,26 @@ "streamsearch": "^1.1.0" } }, + "c12": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/c12/-/c12-1.11.1.tgz", + "integrity": "sha512-KDU0TvSvVdaYcQKQ6iPHATGz/7p/KiVjPg4vQrB6Jg/wX9R0yl5RZxWm9IoZqaIHD2+6PZd81+KMGwRr/lRIUg==", + "dev": true, + "requires": { + "chokidar": "^3.6.0", + "confbox": "^0.1.7", + "defu": "^6.1.4", + "dotenv": "^16.4.5", + "giget": "^1.2.3", + "jiti": "^1.21.6", + "mlly": "^1.7.1", + "ohash": "^1.1.3", + "pathe": "^1.1.2", + "perfect-debounce": "^1.0.0", + "pkg-types": "^1.1.1", + "rc9": "^2.1.2" + } + }, "call-bind": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", @@ -7453,12 +7795,6 @@ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==" }, - "camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true - }, "camelcase-css": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", @@ -7480,9 +7816,9 @@ } }, "chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "dev": true, "requires": { "anymatch": "~3.1.2", @@ -7506,6 +7842,21 @@ } } }, + "chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true + }, + "citty": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", + "dev": true, + "requires": { + "consola": "^3.2.3" + } + }, "cli-cursor": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", @@ -7588,6 +7939,18 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, + "confbox": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.7.tgz", + "integrity": "sha512-uJcB/FKZtBMCJpK8MQji6bJHgu1tixKPxRLeGkNzBoOZzpnZUJm0jm2/sBDWcuBx1dYgxV4JU+g5hmNxCyAmdA==", + "dev": true + }, + "consola": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.2.3.tgz", + "integrity": "sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==", + "dev": true + }, "cross-env": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", @@ -7702,11 +8065,23 @@ "object-keys": "^1.1.1" } }, + "defu": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "dev": true + }, "dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==" }, + "destr": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.3.tgz", + "integrity": "sha512-2N3BOUU4gYMpTP24s5rF5iP7BDr7uNTCs4ozw3kf/eKfvWSIu93GEBi5m427YoyJoeOzQ5smuu4nNAPGb8idSQ==", + "dev": true + }, "detect-libc": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", @@ -7740,6 +8115,12 @@ "esutils": "^2.0.2" } }, + "dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "dev": true + }, "eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -7754,7 +8135,8 @@ "embla-carousel": { "version": "8.1.3", "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.1.3.tgz", - "integrity": "sha512-GiRpKtzidV3v50oVMly8S+D7iE1r96ttt7fSlvtyKHoSkzrAnVcu8fX3c4j8Ol2hZSQlVfDqDIqdrFPs0u5TWQ==" + "integrity": "sha512-GiRpKtzidV3v50oVMly8S+D7iE1r96ttt7fSlvtyKHoSkzrAnVcu8fX3c4j8Ol2hZSQlVfDqDIqdrFPs0u5TWQ==", + "peer": true }, "embla-carousel-react": { "version": "8.1.3", @@ -7908,6 +8290,7 @@ "version": "8.49.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.49.0.tgz", "integrity": "sha512-jw03ENfm6VJI0jA9U+8H5zfl5b+FvuU3YYvZRdZHOlU2ggJkxrlkJH4HcDrZpj6YwD8kuYqvQM8LyesoazrSOQ==", + "peer": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -8027,6 +8410,7 @@ "version": "2.29.1", "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz", "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==", + "peer": true, "requires": { "array-includes": "^3.1.7", "array.prototype.findlastindex": "^1.2.3", @@ -8351,15 +8735,24 @@ "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", "dev": true }, - "fs-extra": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", - "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", "dev": true, "requires": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" + "minipass": "^3.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + } } }, "fs.realpath": { @@ -8436,6 +8829,22 @@ "resolve-pkg-maps": "^1.0.0" } }, + "giget": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/giget/-/giget-1.2.3.tgz", + "integrity": "sha512-8EHPljDvs7qKykr6uw8b+lqLiUc/vUg+KVTI0uND4s63TdsZM2Xus3mflvF0DDG9SiM4RlCkFGL+7aAjRmV7KA==", + "dev": true, + "requires": { + "citty": "^0.1.6", + "consola": "^3.2.3", + "defu": "^6.1.4", + "node-fetch-native": "^1.6.3", + "nypm": "^0.3.8", + "ohash": "^1.1.3", + "pathe": "^1.1.2", + "tar": "^6.2.0" + } + }, "glob": { "version": "7.1.7", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", @@ -8876,9 +9285,9 @@ } }, "jiti": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz", - "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==", + "version": "1.21.6", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz", + "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==", "dev": true }, "jose": { @@ -8922,16 +9331,6 @@ "minimist": "^1.2.0" } }, - "jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.6", - "universalify": "^2.0.0" - } - }, "jsx-ast-utils": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", @@ -9167,6 +9566,45 @@ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==" }, + "minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "requires": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + } + } + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true + }, + "mlly": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.1.tgz", + "integrity": "sha512-rrVRZRELyQzrIUAVMHxP97kv+G786pHmOKzuFII8zDYahFBS7qnHh2AlYSl1GAHhaMPCz6/oHjVMcfFYgFYHgA==", + "dev": true, + "requires": { + "acorn": "^8.11.3", + "pathe": "^1.1.2", + "pkg-types": "^1.1.1", + "ufo": "^1.5.3" + } + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -9203,6 +9641,7 @@ "version": "14.2.4", "resolved": "https://registry.npmjs.org/next/-/next-14.2.4.tgz", "integrity": "sha512-R8/V7vugY+822rsQGQCjoLhMuC9oFj9SOi4Cl4b2wjDrseD0LRZ10W7R6Czo4w9ZznVSshKjuIomsRjvm9EKJQ==", + "peer": true, "requires": { "@next/env": "14.2.4", "@next/swc-darwin-arm64": "14.2.4", @@ -9243,6 +9682,12 @@ "prop-types": "^15.8.1" } }, + "node-fetch-native": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.4.tgz", + "integrity": "sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==", + "dev": true + }, "node-releases": { "version": "2.0.14", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", @@ -9283,6 +9728,20 @@ "resolved": "https://registry.npmjs.org/nprogress/-/nprogress-0.2.0.tgz", "integrity": "sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==" }, + "nypm": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.3.9.tgz", + "integrity": "sha512-BI2SdqqTHg2d4wJh8P9A1W+bslg33vOE9IZDY6eR2QC+Pu1iNBVZUqczrd43rJb+fMzHU7ltAYKsEFY/kHMFcw==", + "dev": true, + "requires": { + "citty": "^0.1.6", + "consola": "^3.2.3", + "execa": "^8.0.1", + "pathe": "^1.1.2", + "pkg-types": "^1.1.1", + "ufo": "^1.5.3" + } + }, "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -9374,6 +9833,12 @@ "es-abstract": "^1.22.1" } }, + "ohash": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-1.1.3.tgz", + "integrity": "sha512-zuHHiGTYTA1sYJ/wZN+t5HKZaH23i4yI1HMwbuXm24Nid7Dv0KcuRlKoNKS9UNfAVSBlnGLcuQrnOKWOZoEGaw==", + "dev": true + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -9391,27 +9856,6 @@ "mimic-fn": "^4.0.0" } }, - "openapi-typescript-codegen": { - "version": "0.29.0", - "resolved": "https://registry.npmjs.org/openapi-typescript-codegen/-/openapi-typescript-codegen-0.29.0.tgz", - "integrity": "sha512-/wC42PkD0LGjDTEULa/XiWQbv4E9NwLjwLjsaJ/62yOsoYhwvmBR31kPttn1DzQ2OlGe5stACcF/EIkZk43M6w==", - "dev": true, - "requires": { - "@apidevtools/json-schema-ref-parser": "^11.5.4", - "camelcase": "^6.3.0", - "commander": "^12.0.0", - "fs-extra": "^11.2.0", - "handlebars": "^4.7.8" - }, - "dependencies": { - "commander": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", - "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", - "dev": true - } - } - }, "optionator": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", @@ -9488,6 +9932,18 @@ "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==" }, + "pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true + }, + "perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "dev": true + }, "picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -9516,11 +9972,23 @@ "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", "dev": true }, + "pkg-types": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.1.3.tgz", + "integrity": "sha512-+JrgthZG6m3ckicaOB74TwQ+tBWsFl3qVQg7mN8ulwSOElJ7gBhKzj2VkCPnZ4NlF6kEquYU+RIYNVAvzd54UA==", + "dev": true, + "requires": { + "confbox": "^0.1.7", + "mlly": "^1.7.1", + "pathe": "^1.1.2" + } + }, "postcss": { "version": "8.4.32", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.32.tgz", "integrity": "sha512-D/kj5JNu6oo2EIy+XL/26JEDTlIbB8hw85G8StOE6L74RQAVVP5rej6wxCNqyMbR4RkPfqvezVbPw81Ngd6Kcw==", "dev": true, + "peer": true, "requires": { "nanoid": "^3.3.7", "picocolors": "^1.0.0", @@ -9599,7 +10067,8 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.1.tgz", "integrity": "sha512-22UbSzg8luF4UuZtzgiUOfcGM8s4tjBv6dJRT7j275NXsy2jb4aJa4NNveul5x4eqlF1wuhuR2RElK71RvmVaw==", - "dev": true + "dev": true, + "peer": true }, "prettier-plugin-tailwindcss": { "version": "0.5.9", @@ -9658,10 +10127,21 @@ "fast-diff": "1.1.2" } }, + "rc9": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", + "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", + "dev": true, + "requires": { + "defu": "^6.1.4", + "destr": "^2.0.3" + } + }, "react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "peer": true, "requires": { "loose-envify": "^1.1.0" } @@ -9682,6 +10162,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "peer": true, "requires": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -9718,6 +10199,7 @@ "version": "9.1.2", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.1.2.tgz", "integrity": "sha512-0OA4dhM1W48l3uzmv6B7TXPCGmokUU4p1M44DGN2/D9a1FjVPukVjER1PcPX97jIg6aUeLq1XJo1IpfbgULn0w==", + "peer": true, "requires": { "@types/use-sync-external-store": "^0.0.3", "use-sync-external-store": "^1.0.0" @@ -9752,7 +10234,8 @@ "redux": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", - "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==" + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "peer": true }, "redux-thunk": { "version": "3.1.0", @@ -10283,6 +10766,28 @@ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==" }, + "tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dev": true, + "requires": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "dependencies": { + "minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true + } + } + }, "text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -10401,12 +10906,19 @@ "typescript": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", - "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==" + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "peer": true + }, + "ufo": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.4.tgz", + "integrity": "sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==", + "dev": true }, "uglify-js": { - "version": "3.17.4", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", - "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==", + "version": "3.19.2", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.2.tgz", + "integrity": "sha512-S8KA6DDI47nQXJSi2ctQ629YzwOVs+bQML6DAtvy0wgNdpi+0ySpQK0g2pxBq2xfF2z3YCscu7NNA8nXT9PlIQ==", "dev": true, "optional": true }, @@ -10421,12 +10933,6 @@ "which-boxed-primitive": "^1.0.2" } }, - "universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true - }, "update-browserslist-db": { "version": "1.0.13", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", @@ -10590,6 +11096,12 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, "yaml": { "version": "2.3.4", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", diff --git a/package.json b/package.json index 1414b42..f8e3ff0 100644 --- a/package.json +++ b/package.json @@ -12,9 +12,11 @@ "prepare": "husky install", "open:api": "curl https://api.aimcup.xyz/v3/api-docs -o openapi.json", "open:api:local": "curl http://localhost:8080/v3/api-docs -o openapi.json", - "open:generate": "openapi --input ./openapi.json --output ./generated" + "open:generate": "openapi --input ./openapi.json --output ./generated", + "openapi-ts": "openapi-ts" }, "dependencies": { + "@hey-api/client-fetch": "^0.2.4", "@next/font": "^14.2.4", "@reduxjs/toolkit": "^2.2.5", "@types/node": "20.6.0", @@ -41,6 +43,7 @@ "zod": "^3.23.8" }, "devDependencies": { + "@hey-api/openapi-ts": "^0.52.8", "@typescript-eslint/eslint-plugin": "^6.14.0", "@typescript-eslint/parser": "^6.14.0", "autoprefixer": "^10.4.16", @@ -50,7 +53,6 @@ "eslint-plugin-import": "^2.29.1", "husky": "^8.0.3", "lint-staged": "^15.2.0", - "openapi-typescript-codegen": "^0.29.0", "postcss": "^8.4.32", "prettier": "^3.1.1", "prettier-plugin-tailwindcss": "^0.5.9", diff --git a/src/actions/admin/adminAddMatchActions.ts b/src/actions/admin/adminAddMatchActions.ts new file mode 100644 index 0000000..86d7fe5 --- /dev/null +++ b/src/actions/admin/adminAddMatchActions.ts @@ -0,0 +1,53 @@ +"use server"; + +import { cookies } from "next/headers"; +import { client, createMatch, type stageType } from "../../../client"; +import { multipleRevalidatePaths } from "@/lib/multipleRevalidatePaths"; +import { type AddMachSchemaType } from "@/formSchemas/addMachSchema"; + +export async function addMatchAction(formData: AddMachSchemaType) { + "use server"; + const cookie = cookies().get("JWT")?.value; + // configure internal service client + client.setConfig({ + // set default base url for requests + baseUrl: process.env.NEXT_PUBLIC_API_URL, + // set default headers for requests + headers: { + Cookie: `token=${cookie}`, + }, + }); + const { data, error } = await createMatch({ + path: { + abbreviation: formData.tournamentAbbreviation, + }, + body: { + startDate: formData.dataTimeStart, + matchId: formData.matchId, + commentatorIds: formData.commentatorIds, + refereeIds: formData.refereeIds, + streamerIds: formData.streamerIds, + stageType: formData.stageType as stageType, + teamBlueId: formData.teamBlueId, + teamRedId: formData.teamRedId, + }, + }); + + if (error) { + return { + status: false as const, + errorMessage: error.errors?.map((e) => e).join(", "), + }; + } + + await multipleRevalidatePaths([ + "/", + `/dashboard/${formData.tournamentAbbreviation}/matches`, + `/tournament/${formData.tournamentAbbreviation}/schedule`, + ]); + + return { + status: true as const, + response: data, + }; +} diff --git a/src/actions/admin/adminBeatMapActions.ts b/src/actions/admin/adminBeatMapActions.ts index 9dd3894..bf968ce 100644 --- a/src/actions/admin/adminBeatMapActions.ts +++ b/src/actions/admin/adminBeatMapActions.ts @@ -1,33 +1,49 @@ "use server"; - -import { - AdminMappoolService, - type BeatmapModificationResponseDto, - type BeatmapResponseDto, -} from "../../../generated"; -import { type ErrorResponse, executeFetch, type SuccessfulResponse } from "@/lib/executeFetch"; +import { cookies } from "next/headers"; +import { addBeatmap, client, type modification, type stageType } from "../../../client"; import { type AddBeatMapSchemaType } from "@/formSchemas/addBeatMapSchema"; +import { multipleRevalidatePaths } from "@/lib/multipleRevalidatePaths"; -export async function addBeatMapAction(data: AddBeatMapSchemaType) { +export async function addBeatMapAction(formData: AddBeatMapSchemaType, stageType: stageType) { "use server"; + const cookie = cookies().get("JWT")?.value; + // configure internal service client + client.setConfig({ + // set default base url for requests + baseUrl: process.env.NEXT_PUBLIC_API_URL, + // set default headers for requests + headers: { + Cookie: `token=${cookie}`, + }, + }); + const { data, error } = await addBeatmap({ + path: { + abbreviation: formData.tournamentAbb, + modification: formData.modification as modification, + mappoolId: formData.mappoolId, + }, + body: { + url: formData.url, + position: +formData.position, + isCustom: formData?.isCustom === "on", + }, + }); + + if (error) { + return { + status: false as const, + errorMessage: error.errors?.map((e) => e).join(", "), + }; + } + + await multipleRevalidatePaths([ + "/", + `/dashboard/${formData.tournamentAbb}/mappool/${stageType}/${formData.mappoolId}`, + `/tournament/${formData.tournamentAbb}/mappool/${stageType}`, + ]); - return executeFetch( - AdminMappoolService.addBeatmap( - data.tournamentAbb, - data.mappoolId, - data.modification as BeatmapModificationResponseDto.modification, - { - url: data.url, - position: +data.position, - isCustom: data?.isCustom === "on", - }, - ), - ["/", "/dashboard/[tournamentAbb]/mappool/[stageType]/[mappoolId]"], - ) - .then((res) => { - return res as SuccessfulResponse; - }) - .catch((error) => { - return error as ErrorResponse; - }); + return { + status: true as const, + response: data, + }; } diff --git a/src/actions/admin/adminDeleteMatchActions.ts b/src/actions/admin/adminDeleteMatchActions.ts new file mode 100644 index 0000000..9754e28 --- /dev/null +++ b/src/actions/admin/adminDeleteMatchActions.ts @@ -0,0 +1,46 @@ +"use server"; + +import { cookies } from "next/headers"; +import { client, deleteMatch } from "../../../client"; +import { multipleRevalidatePaths } from "@/lib/multipleRevalidatePaths"; + +export async function deleteMatchAction(tournamentAbbreviation: string, matchId: string) { + "use server"; + const cookie = cookies().get("JWT")?.value; + // configure internal service client + client.setConfig({ + // set default base url for requests + baseUrl: process.env.NEXT_PUBLIC_API_URL, + // set default headers for requests + headers: { + Cookie: `token=${cookie}`, + }, + }); + const { error } = await deleteMatch({ + path: { + abbreviation: tournamentAbbreviation, + matchId: matchId, + }, + }); + + if (error) { + return { + status: false as const, + errorMessage: error.errors?.map((e) => e).join(", "), + }; + } + + await multipleRevalidatePaths([ + "/", + `/dashboard/${tournamentAbbreviation}/matches`, + `/tournament/${tournamentAbbreviation}/schedule`, + ]); + + return { + status: true as const, + }; +} + + + + diff --git a/src/actions/admin/adminEditMatchActions.ts b/src/actions/admin/adminEditMatchActions.ts new file mode 100644 index 0000000..de87798 --- /dev/null +++ b/src/actions/admin/adminEditMatchActions.ts @@ -0,0 +1,52 @@ +"use server"; + +import { cookies } from "next/headers"; +import { client, updateMatch, type MatchRequestDto } from "../../../client"; +import { multipleRevalidatePaths } from "@/lib/multipleRevalidatePaths"; +import { type EditMatchSchemaType } from "@/formSchemas/editMatchSchema"; + +export async function editMatchAction(formData: EditMatchSchemaType) { + "use server"; + const cookie = cookies().get("JWT")?.value; + // configure internal service client + client.setConfig({ + // set default base url for requests + baseUrl: process.env.NEXT_PUBLIC_API_URL, + // set default headers for requests + headers: { + Cookie: `token=${cookie}`, + }, + }); + const { data, error } = await updateMatch({ + path: { + abbreviation: formData.tournamentAbbreviation, + matchId: formData.matchId, + }, + body: { + startDate: formData.dataTimeStart, + stageType: formData.stageType as MatchRequestDto["stageType"], + commentatorIds: formData.commentatorIds ?? [], + refereeIds: formData.refereeIds ?? [], + streamerIds: formData.streamerIds ?? [], + }, + }); + + if (error) { + return { + status: false as const, + errorMessage: error.errors?.map((e) => e).join(", "), + }; + } + + await multipleRevalidatePaths([ + "/", + `/dashboard/${formData.tournamentAbbreviation}/matches`, + `/tournament/${formData.tournamentAbbreviation}/schedule`, + ]); + + return { + status: true as const, + response: data, + }; +} + diff --git a/src/actions/admin/adminQualificationRoomsActions.ts b/src/actions/admin/adminQualificationRoomsActions.ts index 442c704..ec6822a 100644 --- a/src/actions/admin/adminQualificationRoomsActions.ts +++ b/src/actions/admin/adminQualificationRoomsActions.ts @@ -1,48 +1,90 @@ "use server"; -import { AdminQualificationService, type AdminStaffMemberService } from "../../../generated"; -import { type ErrorResponse, executeFetch, type SuccessfulResponse } from "@/lib/executeFetch"; +import { cookies } from "next/headers"; +import { client, createQualificationRooms, updateQualificationRoom } from "../../../client"; import { type CreateQualificationRoomsSchemaType } from "@/formSchemas/createQualificationRoomsSchema"; import { type EditQualificationRoomsSchemaType } from "@/formSchemas/editQualificationRoomSchema"; +import { multipleRevalidatePaths } from "@/lib/multipleRevalidatePaths"; -export async function createQualificationRoomsAction(data: CreateQualificationRoomsSchemaType) { +export async function createQualificationRoomsAction(formData: CreateQualificationRoomsSchemaType) { "use server"; + const cookie = cookies().get("JWT")?.value; + // configure internal service client + client.setConfig({ + // set default base url for requests + baseUrl: process.env.NEXT_PUBLIC_API_URL, + // set default headers for requests + headers: { + Cookie: `token=${cookie}`, + }, + }); + const { data, error } = await createQualificationRooms({ + path: { + abbreviation: formData.tournamentAbbreviation, + }, + body: { + amount: +formData.amount, + timeOffset: +formData.offset, + startDateTime: formData.dataTimeStart, + }, + }); - return executeFetch( - AdminQualificationService.createQualificationRooms(data.tournamentAbbreviation, { - amount: +data.amount, - timeOffset: +data.offset, - startDateTime: data.dataTimeStart, - }), - ["/", "/dashboard/[tournamentAbb]/qualification-rooms"], - ) - .then((res) => { - return res as SuccessfulResponse; - }) - .catch((error) => { - return error as ErrorResponse; - }); + if (error) { + return { + status: false as const, + errorMessage: error.errors?.map((e) => e).join(", "), + }; + } + + await multipleRevalidatePaths([ + "/", + `/dashboard/${formData.tournamentAbbreviation}/qualification-rooms`, + ]); + + return { + status: true as const, + response: data, + }; } -export async function editQualificationRoomsAction(data: EditQualificationRoomsSchemaType) { +export async function editQualificationRoomsAction(formData: EditQualificationRoomsSchemaType) { "use server"; + const cookie = cookies().get("JWT")?.value; + // configure internal service client + client.setConfig({ + // set default base url for requests + baseUrl: process.env.NEXT_PUBLIC_API_URL, + // set default headers for requests + headers: { + Cookie: `token=${cookie}`, + }, + }); + const { data, error } = await updateQualificationRoom({ + path: { + abbreviation: formData.tournamentAbbreviation, + roomId: formData.roomId, + }, + body: { + rosterIds: formData.rosterIds?.map((roster) => roster), + staffMemberId: formData.staffMemberId, + startDate: formData.dataTimeStart, + }, + }); + + if (error) { + return { + status: false as const, + errorMessage: error.errors?.map((e) => e).join(", "), + }; + } + + await multipleRevalidatePaths([ + "/", + `/dashboard/${formData.tournamentAbbreviation}/qualification-rooms`, + ]); - return executeFetch( - AdminQualificationService.updateQualificationRoom( - data.tournamentAbbreviation, - data.roomId, - { - rosterIds: data.rosterIds, - staffMemberId: data.staffMemberId, - startDate: data.dataTimeStart, - }, - ), - ["/", `/dashboard/${data.tournamentAbbreviation}/qualification-rooms`], - ) - .then((res) => { - return res as SuccessfulResponse; - }) - .catch((error) => { - return error as ErrorResponse; - }); + return { + status: true as const, + response: data, + }; } diff --git a/src/actions/admin/adminStaffMembersActions.ts b/src/actions/admin/adminStaffMembersActions.ts index 05b4dea..eeb87e5 100644 --- a/src/actions/admin/adminStaffMembersActions.ts +++ b/src/actions/admin/adminStaffMembersActions.ts @@ -1,65 +1,133 @@ "use server"; -import { AdminStaffMemberService } from "../../../generated"; -import { type ErrorResponse, executeFetch, type SuccessfulResponse } from "@/lib/executeFetch"; +import { cookies } from "next/headers"; +import { addStaffMembers, client, updateStaffMembers } from "../../../client"; import { type AddStaffMembersSchemaType } from "@/formSchemas/addEditStaffMembersSchema"; import { type AddUserLessStaffMembersSchemaType } from "@/formSchemas/addEditUserLessStaffMembersSchema"; +import { multipleRevalidatePaths } from "@/lib/multipleRevalidatePaths"; -export async function editStaffMemberAction(data: AddStaffMembersSchemaType) { +export async function editStaffMemberAction(formData: AddStaffMembersSchemaType) { "use server"; + const cookie = cookies().get("JWT")?.value; + // configure internal service client + client.setConfig({ + // set default base url for requests + baseUrl: process.env.NEXT_PUBLIC_API_URL, + // set default headers for requests + headers: { + Cookie: `token=${cookie}`, + }, + }); + const { data, error } = await updateStaffMembers({ + path: { + staffMemberId: formData.osuId, + abbreviation: formData.tournamentAbbreviation, + }, + body: { + discordId: formData.discordId, + roles: formData.roles, + permissions: formData.permissions, + }, + }); - return executeFetch( - AdminStaffMemberService.updateStaffMembers(data.tournamentAbbreviation, data.osuId, { - discordId: data.discordId, - roles: data.roles, - permissions: data.permissions, - }), - ["/", `/dashboard/${data.tournamentAbbreviation}/staff-members`], - ) - .then((res) => { - return res as SuccessfulResponse; - }) - .catch((error) => { - return error as ErrorResponse; - }); + if (error) { + return { + status: false as const, + // errorMessage: error.errors?.map((e) => e).join(", "), //todo + }; + } + + await multipleRevalidatePaths([ + "/", + `/dashboard/${formData.tournamentAbbreviation}/staff-members`, + ]); + + return { + status: true as const, + response: data, + }; } -export async function addStaffMemberAction(data: AddStaffMembersSchemaType) { +export async function addStaffMemberAction(formData: AddStaffMembersSchemaType) { "use server"; + const cookie = cookies().get("JWT")?.value; + // configure internal service client + client.setConfig({ + // set default base url for requests + baseUrl: process.env.NEXT_PUBLIC_API_URL, + // set default headers for requests + headers: { + Cookie: `token=${cookie}`, + }, + }); + const { data, error } = await addStaffMembers({ + path: { + abbreviation: formData.tournamentAbbreviation, + }, + body: { + osuId: formData.osuId, + discordId: formData.discordId, + roles: formData.roles, + permissions: formData.permissions, + }, + }); + + if (error) { + return { + status: false as const, + // errorMessage: error.errors?.map((e) => e).join(", "), //todo + }; + } - return executeFetch( - AdminStaffMemberService.addStaffMembers(data.tournamentAbbreviation, { - osuId: data.osuId, - discordId: data.discordId, - roles: data.roles || [], - permissions: data.permissions || [], - }), - ["/", `/dashboard/${data.tournamentAbbreviation}/staff-members`], - ) - .then((res) => { - return res as SuccessfulResponse; - }) - .catch((error) => { - return error as ErrorResponse; - }); + await multipleRevalidatePaths([ + "/", + `/dashboard/${formData.tournamentAbbreviation}/staff-members`, + ]); + + return { + status: true as const, + response: data, + }; } -export async function addUserLessStaffMemberAction(data: AddUserLessStaffMembersSchemaType) { +export async function addUserLessStaffMemberAction(formData: AddUserLessStaffMembersSchemaType) { "use server"; + const cookie = cookies().get("JWT")?.value; + // configure internal service client + client.setConfig({ + // set default base url for requests + baseUrl: process.env.NEXT_PUBLIC_API_URL, + // set default headers for requests + headers: { + Cookie: `token=${cookie}`, + }, + }); + const { data, error } = await addStaffMembers({ + path: { + abbreviation: formData.tournamentAbbreviation, + }, + body: { + username: formData.username, + roles: formData.roles, + redirectUrl: formData.redirectUrl, + imageUrl: formData.imageUrl, + }, + }); + + if (error) { + return { + status: false as const, + // errorMessage: error.errors?.map((e) => e).join(", "), //todo + }; + } + + await multipleRevalidatePaths([ + "/", + `/dashboard/${formData.tournamentAbbreviation}/staff-members`, + ]); - return executeFetch( - AdminStaffMemberService.addStaffMembers(data.tournamentAbbreviation, { - roles: data.roles || [], - username: data.username, - redirectUrl: data.redirectUrl, - imageUrl: data.imageUrl, - }), - ["/", `/dashboard/${data.tournamentAbbreviation}/staff-members`], - ) - .then((res) => { - return res as SuccessfulResponse; - }) - .catch((error) => { - return error as ErrorResponse; - }); + return { + status: true as const, + response: data, + }; } diff --git a/src/actions/admin/adminStageActions.ts b/src/actions/admin/adminStageActions.ts index 8646813..486e349 100644 --- a/src/actions/admin/adminStageActions.ts +++ b/src/actions/admin/adminStageActions.ts @@ -1,48 +1,92 @@ "use server"; -import { AdminStageService, type StageResponseDto } from "../../../generated"; -import { type ErrorResponse, executeFetch, type SuccessfulResponse } from "@/lib/executeFetch"; +import { cookies } from "next/headers"; +import { client, createStage, type stageType, updateStage } from "../../../client"; import { type CreateStageSchemaType, type EditStageSchemaType, } from "@/formSchemas/createStageSchema"; +import { multipleRevalidatePaths } from "@/lib/multipleRevalidatePaths"; -export async function createStageAction(data: CreateStageSchemaType) { +export async function createStageAction(formData: CreateStageSchemaType) { "use server"; + const cookie = cookies().get("JWT")?.value; + // configure internal service client + client.setConfig({ + // set default base url for requests + baseUrl: process.env.NEXT_PUBLIC_API_URL, + // set default headers for requests + headers: { + Cookie: `token=${cookie}`, + }, + }); + const { data, error } = await createStage({ + path: { + abbreviation: formData.tournamentAbb, + }, + body: { + startDate: new Date(formData.startDate).toISOString(), + endDate: new Date(formData.endDate).toISOString(), + stageType: formData.stageType as stageType, + }, + }); - return executeFetch( - AdminStageService.createStage(data.tournamentAbb, { - endDate: new Date(data.endDate).toISOString(), - stageType: data.stageType as StageResponseDto.stageType, - startDate: new Date(data.startDate).toISOString(), - }), - ["/", `/dashboard/${data.tournamentAbb}`, `/tournament/${data.tournamentAbb}`], - ) - .then((res) => { - return res as SuccessfulResponse; - }) - .catch((error) => { - return error as ErrorResponse; - }); + if (error) { + return { + status: false as const, + errorMessage: error.errors?.map((e) => e).join(", "), + }; + } + + await multipleRevalidatePaths([ + "/", + `/dashboard/${formData.tournamentAbb}`, + `/tournament/${formData.tournamentAbb}`, + ]); + + return { + status: true as const, + response: data, + }; } -export async function editStageAction(data: EditStageSchemaType) { +export async function editStageAction(formData: EditStageSchemaType) { "use server"; + const cookie = cookies().get("JWT")?.value; + // configure internal service client + client.setConfig({ + // set default base url for requests + baseUrl: process.env.NEXT_PUBLIC_API_URL, + // set default headers for requests + headers: { + Cookie: `token=${cookie}`, + }, + }); + const { data, error } = await updateStage({ + path: { + abbreviation: formData.tournamentAbb, + stageType: formData.stageType as stageType, + }, + body: { + startDate: new Date(formData.startDate).toISOString(), + endDate: new Date(formData.endDate).toISOString(), + }, + }); + + if (error) { + return { + status: false as const, + errorMessage: error.errors?.map((e) => e).join(", "), + }; + } + + await multipleRevalidatePaths([ + "/", + `/dashboard/${formData.tournamentAbb}`, + `/tournament/${formData.tournamentAbb}`, + ]); - return executeFetch( - AdminStageService.updateStage( - data.tournamentAbb, - data.stageType as StageResponseDto.stageType, - { - endDate: new Date(data.endDate).toISOString(), - startDate: new Date(data.startDate).toISOString(), - }, - ), - ["/", `/dashboard/${data.tournamentAbb}`, `/tournament/${data.tournamentAbb}`], - ) - .then((res) => { - return res as SuccessfulResponse; - }) - .catch((error) => { - return error as ErrorResponse; - }); + return { + status: true as const, + response: data, + }; } diff --git a/src/actions/admin/adminTeamActions.ts b/src/actions/admin/adminTeamActions.ts new file mode 100644 index 0000000..e682b0f --- /dev/null +++ b/src/actions/admin/adminTeamActions.ts @@ -0,0 +1,298 @@ +"use server"; + +import { cookies } from "next/headers"; +import { client, addParticipantToTeam, removeParticipantFromTeam, changeTeamCaptain, deleteTeam, changeTeamStatus, type ErrorResponse } from "../../../client"; +import { multipleRevalidatePaths } from "@/lib/multipleRevalidatePaths"; + +// Type guard to check if error is ErrorResponse +function isErrorResponse(error: unknown): error is ErrorResponse { + return typeof error === "object" && error !== null && "errors" in error; +} + +export async function updateTeamAction(formDataToSend: FormData) { + "use server"; + const cookie = cookies().get("JWT")?.value; + const apiUrl = process.env.NEXT_PUBLIC_API_URL; + + if (!apiUrl) { + return { + status: false as const, + errorMessage: "API URL is not configured", + }; + } + + const tournamentAbbreviation = formDataToSend.get("tournamentAbbreviation") as string; + const teamId = formDataToSend.get("teamId") as string; + const name = formDataToSend.get("name") as string; + + if (!tournamentAbbreviation || !teamId || !name) { + return { + status: false as const, + errorMessage: "Missing required fields", + }; + } + + // Create new FormData with only the fields needed for the API + const apiFormData = new FormData(); + apiFormData.append("name", name); + const logo = formDataToSend.get("logo"); + if (logo instanceof File && logo.size > 0) { + apiFormData.append("logo", logo); + } + + // Use fetch directly for multipart/form-data + const response = await fetch( + `${apiUrl}/admin/tournaments/${tournamentAbbreviation}/teams/${teamId}`, + { + method: "PATCH", + headers: { + Cookie: `token=${cookie}`, + }, + body: apiFormData, + } + ); + + if (!response.ok) { + const errorText = await response.text(); + let errorData; + try { + errorData = JSON.parse(errorText); + } catch { + errorData = { errors: [errorText || "Failed to update team"] }; + } + return { + status: false as const, + errorMessage: errorData.errors?.map((e: string) => e).join(", ") || errorData.message || "Failed to update team", + }; + } + + await multipleRevalidatePaths([ + "/", + `/dashboard/${tournamentAbbreviation}/teams`, + `/tournament/${tournamentAbbreviation}/teams/${teamId}`, + `/account/`, + ]); + + return { + status: true as const, + }; +} + +export async function addParticipantAction( + tournamentAbbreviation: string, + teamId: string, + osuId: string, +) { + "use server"; + const cookie = cookies().get("JWT")?.value; + client.setConfig({ + baseUrl: process.env.NEXT_PUBLIC_API_URL, + headers: { + Cookie: `token=${cookie}`, + }, + }); + + const { data, error } = await addParticipantToTeam({ + path: { + abbreviation: tournamentAbbreviation, + teamId: teamId, + osuId: osuId, + }, + }); + + if (error) { + const errorMessage: string = isErrorResponse(error) + ? (error.errors?.join(", ") || "Failed to add participant") + : "Failed to add participant"; + return { + status: false as const, + errorMessage, + }; + } + + await multipleRevalidatePaths([ + "/", + `/dashboard/${tournamentAbbreviation}/teams`, + `/tournament/${tournamentAbbreviation}/teams/${teamId}`, + ]); + + return { + status: true as const, + team: data, + }; +} + +export async function removeParticipantAction( + tournamentAbbreviation: string, + teamId: string, + osuId: string, +) { + "use server"; + const cookie = cookies().get("JWT")?.value; + client.setConfig({ + baseUrl: process.env.NEXT_PUBLIC_API_URL, + headers: { + Cookie: `token=${cookie}`, + }, + }); + + const { data, error } = await removeParticipantFromTeam({ + path: { + abbreviation: tournamentAbbreviation, + teamId: teamId, + osuId: osuId, + }, + }); + + if (error) { + const errorMessage: string = isErrorResponse(error) + ? (error.errors?.join(", ") || "Failed to remove participant") + : "Failed to remove participant"; + return { + status: false as const, + errorMessage, + }; + } + + await multipleRevalidatePaths([ + "/", + `/dashboard/${tournamentAbbreviation}/teams`, + `/tournament/${tournamentAbbreviation}/teams/${teamId}`, + ]); + + return { + status: true as const, + team: data, + }; +} + +export async function changeCaptainAction( + tournamentAbbreviation: string, + teamId: string, + osuId: string, +) { + "use server"; + const cookie = cookies().get("JWT")?.value; + client.setConfig({ + baseUrl: process.env.NEXT_PUBLIC_API_URL, + headers: { + Cookie: `token=${cookie}`, + }, + }); + + const { data, error } = await changeTeamCaptain({ + path: { + abbreviation: tournamentAbbreviation, + teamId: teamId, + osuId: osuId, + }, + }); + + if (error) { + const errorMessage: string = isErrorResponse(error) + ? (error.errors?.join(", ") || "Failed to change captain") + : "Failed to change captain"; + return { + status: false as const, + errorMessage, + }; + } + + await multipleRevalidatePaths([ + "/", + `/dashboard/${tournamentAbbreviation}/teams`, + `/tournament/${tournamentAbbreviation}/teams/${teamId}`, + ]); + + return { + status: true as const, + team: data, + }; +} + +export async function deleteTeamAction( + tournamentAbbreviation: string, + teamId: string, +) { + "use server"; + const cookie = cookies().get("JWT")?.value; + client.setConfig({ + baseUrl: process.env.NEXT_PUBLIC_API_URL, + headers: { + Cookie: `token=${cookie}`, + }, + }); + + const { error } = await deleteTeam({ + path: { + abbreviation: tournamentAbbreviation, + teamId: teamId, + }, + }); + + if (error) { + const errorMessage: string = isErrorResponse(error) + ? (error.errors?.join(", ") || "Failed to delete team") + : "Failed to delete team"; + return { + status: false as const, + errorMessage, + }; + } + + await multipleRevalidatePaths([ + "/", + `/dashboard/${tournamentAbbreviation}/teams`, + `/account/`, + ]); + + return { + status: true as const, + }; +} + +export async function changeTeamStatusAction( + tournamentAbbreviation: string, + teamId: string, + status: "ACCEPTED" | "REJECTED", +) { + "use server"; + const cookie = cookies().get("JWT")?.value; + client.setConfig({ + baseUrl: process.env.NEXT_PUBLIC_API_URL, + headers: { + Cookie: `token=${cookie}`, + }, + }); + + const { error } = await changeTeamStatus({ + path: { + abbreviation: tournamentAbbreviation, + teamId: teamId, + }, + query: { + status: status, + }, + }); + + if (error) { + const errorMessage: string = isErrorResponse(error) + ? (error.errors?.join(", ") || "Failed to change team status") + : "Failed to change team status"; + return { + status: false as const, + errorMessage, + }; + } + + await multipleRevalidatePaths([ + "/", + `/dashboard/${tournamentAbbreviation}/teams`, + `/tournament/${tournamentAbbreviation}/teams/${teamId}`, + `/account/`, + ]); + + return { + status: true as const, + }; +} diff --git a/src/actions/admin/editTournamentAction.ts b/src/actions/admin/editTournamentAction.ts index 732bb32..dd01cbe 100644 --- a/src/actions/admin/editTournamentAction.ts +++ b/src/actions/admin/editTournamentAction.ts @@ -1,34 +1,54 @@ "use server"; -import { AdminTournamentService, type UpdateTournamentDataRequestDto } from "../../../generated"; -import { type ErrorResponse, executeFetch, type SuccessfulResponse } from "@/lib/executeFetch"; +import { cookies } from "next/headers"; +import { client, updateTournament } from "../../../client"; import { type EditTournamentSchemaType } from "@/formSchemas/editTournamentSchema"; +import { multipleRevalidatePaths } from "@/lib/multipleRevalidatePaths"; -export async function editTournamentAction(data: EditTournamentSchemaType, rules: string) { +export async function editTournamentAction(formData: EditTournamentSchemaType, formRules: string) { "use server"; - - return executeFetch( - AdminTournamentService.updateTournament(data.oldAbbreviation, { + const cookie = cookies().get("JWT")?.value; + // configure internal service client + client.setConfig({ + // set default base url for requests + baseUrl: process.env.NEXT_PUBLIC_API_URL, + // set default headers for requests + headers: { + Cookie: `token=${cookie}`, + }, + }); + const { data, error } = await updateTournament({ + path: { + abbreviation: formData.oldAbbreviation, + }, + body: { + rules: formRules, + name: formData.name, + abbreviation: formData.abbreviation, prizePool: [ - { type: 0, prize: data.prize0 }, - { type: 1, prize: data.prize1 }, - { type: 2, prize: data.prize2 }, + { type: 0, prize: formData.prize0 }, + { type: 1, prize: formData.prize1 }, + { type: 2, prize: formData.prize2 }, ], - abbreviation: data.abbreviation, - name: data.name, - rules: rules, - }), - [ - "/", - `/dashboard/${data.abbreviation}`, - `/dashboard/${data.abbreviation}/settings`, - `/tournament/${data.abbreviation}`, - ], - ) - .then((res) => { - return res as SuccessfulResponse; - }) - .catch((error) => { - return error as ErrorResponse; - }); + }, + }); + + if (error) { + return { + status: false as const, + errorMessage: error.errors?.map((e) => e).join(", "), + }; + } + + await multipleRevalidatePaths([ + "/", + `/dashboard/${formData.abbreviation}`, + `/dashboard/${formData.abbreviation}/settings`, + `/tournament/${formData.abbreviation}`, + ]); + + return { + status: true as const, + response: data, + }; } diff --git a/src/actions/admin/editTournamentImageAction.ts b/src/actions/admin/editTournamentImageAction.ts index 58492c2..49f1598 100644 --- a/src/actions/admin/editTournamentImageAction.ts +++ b/src/actions/admin/editTournamentImageAction.ts @@ -1,34 +1,63 @@ "use server"; -import { AdminTournamentService } from "../../../generated"; -import { executeFetch } from "@/lib/executeFetch"; +import { cookies } from "next/headers"; +import { client, updateTournamentImage } from "../../../client"; +import { multipleRevalidatePaths } from "@/lib/multipleRevalidatePaths"; export async function editTournamentImageAction(formData: FormData) { "use server"; - + const cookie = cookies().get("JWT")?.value; + // configure internal service client + client.setConfig({ + // set default base url for requests + baseUrl: process.env.NEXT_PUBLIC_API_URL, + // set default headers for requests + headers: { + Cookie: `token=${cookie}`, + }, + }); const image = formData.get("image"); if (!image) { - throw new Error("Image is required"); + return { + status: false as const, + errorMessage: "Image is required", + }; } const abbreviation = formData.get("abbreviation"); if (!abbreviation) { - throw new Error("Abbreviation is required"); + return { + status: false as const, + errorMessage: "Abbreviation is required", + }; } - return executeFetch( - AdminTournamentService.updateTournamentImage(abbreviation as string, "BANNER", { + const { data, error } = await updateTournamentImage({ + path: { + abbreviation: abbreviation as string, + }, + query: { + imageType: "BANNER", + }, + body: { image: image as Blob, - }), - ["/", `/dashboard/${abbreviation as string}`, `/tournament/${abbreviation as string}`], - ) - .then((_res) => { - return { - status: true as const, - }; - }) - .catch((_error) => { - return { - status: false as const, - }; - }); + }, + }); + + if (error) { + return { + status: false as const, + errorMessage: error.errors?.map((e) => e).join(", "), + }; + } + + await multipleRevalidatePaths([ + "/", + `/dashboard/${abbreviation as string}`, + `/tournament/${abbreviation as string}`, + ]); + + return { + status: true as const, + response: data, + }; } diff --git a/src/actions/public/createTeamAction.ts b/src/actions/public/createTeamAction.ts index d07d82d..84a43b6 100644 --- a/src/actions/public/createTeamAction.ts +++ b/src/actions/public/createTeamAction.ts @@ -1,73 +1,164 @@ "use server"; -import { type TeamResponseDto, TeamService } from "../../../generated"; +import { cookies } from "next/headers"; +import { client, createTeam, inviteParticipant } from "../../../client"; import { type CreateTeamSchemaType } from "@/formSchemas/createTeamSchema"; -import { type ErrorResponse, executeFetch, type SuccessfulResponse } from "@/lib/executeFetch"; -import { type updateTeamSchemaType } from "@/formSchemas/updateTeamSchema"; import { type inviteToTeamSchemaType } from "@/formSchemas/inviteToTeamSchema"; +import { multipleRevalidatePaths } from "@/lib/multipleRevalidatePaths"; -export async function createTeamAction(data: CreateTeamSchemaType) { +export async function createTeamAction(formData: CreateTeamSchemaType) { "use server"; - if (data.terms === "off") { + const cookie = cookies().get("JWT")?.value; + // configure internal service client + client.setConfig({ + // set default base url for requests + baseUrl: process.env.NEXT_PUBLIC_API_URL, + // set default headers for requests + headers: { + Cookie: `token=${cookie}`, + }, + }); + + if (formData.terms === "off") { return { - status: false, + status: false as const, errorMessage: "You need to accept the terms and conditions", }; } - return executeFetch( - TeamService.createTeam(data.tournamentAbb, { - name: data.teamName, + const { data, error } = await createTeam({ + path: { + abbreviation: formData.tournamentAbb, + }, + body: { + name: formData.teamName, logoUrl: "", - }), - ["/", `/tournament/${data.tournamentAbb}`], - ) - .then((res) => { - return res as SuccessfulResponse; - }) - .catch((error) => { - return error as ErrorResponse; - }); + }, + }); + + if (error) { + return { + status: false as const, + errorMessage: error.errors?.map((e) => e).join(", "), + }; + } + + await multipleRevalidatePaths([ + "/", + `/tournament/${formData.tournamentAbb}`, + `/tournament/${formData.tournamentAbb}/teams`, + ]); + + return { + status: true as const, + response: data, + }; } -export async function updateTeam(data: updateTeamSchemaType) { +export async function updateTeamAction(formDataToSend: FormData) { "use server"; + const cookie = cookies().get("JWT")?.value; + const apiUrl = process.env.NEXT_PUBLIC_API_URL; + + if (!apiUrl) { + return { + status: false as const, + errorMessage: "API URL is not configured", + }; + } + + const tournamentAbbreviation = formDataToSend.get("tournamentAbbreviation") as string; + const teamId = formDataToSend.get("teamId") as string; + const name = formDataToSend.get("name") as string; + + if (!tournamentAbbreviation || !teamId || !name) { + return { + status: false as const, + errorMessage: "Missing required fields", + }; + } + + // Create new FormData with only the fields needed for the API + const apiFormData = new FormData(); + apiFormData.append("name", name); + const logo = formDataToSend.get("logo"); + if (logo instanceof File && logo.size > 0) { + apiFormData.append("logo", logo); + } + + // Use fetch directly for multipart/form-data + const response = await fetch( + `${apiUrl}/tournaments/${tournamentAbbreviation}/teams/${teamId}`, + { + method: "PUT", + headers: { + Cookie: `token=${cookie}`, + }, + body: apiFormData, + } + ); + + if (!response.ok) { + const errorText = await response.text(); + let errorData; + try { + errorData = JSON.parse(errorText); + } catch { + errorData = { errors: [errorText || "Failed to update team"] }; + } + return { + status: false as const, + errorMessage: errorData.errors?.map((e: string) => e).join(", ") || errorData.message || "Failed to update team", + }; + } + + await multipleRevalidatePaths([ + "/", + `/tournament/${tournamentAbbreviation}/teams/${teamId}`, + `/tournament/${tournamentAbbreviation}`, + ]); - return executeFetch( - TeamService.updateTeam(data.tournamentAbbreviation, data.teamId, { - name: data.name, - logoUrl: data.logoUrl, - }), - [ - "/", - `/tournament/${data.tournamentAbbreviation}/teams/${data.teamId}`, - `/tournament/${data.tournamentAbbreviation}`, - ], - ) - .then((res) => { - return res as SuccessfulResponse; - }) - .catch((error) => { - return error as ErrorResponse; - }); + return { + status: true as const, + }; } -export async function inviteToTeam(data: inviteToTeamSchemaType) { +export async function inviteToTeamAction(formData: inviteToTeamSchemaType) { "use server"; + const cookie = cookies().get("JWT")?.value; + // configure internal service client + client.setConfig({ + // set default base url for requests + baseUrl: process.env.NEXT_PUBLIC_API_URL, + // set default headers for requests + headers: { + Cookie: `token=${cookie}`, + }, + }); + const { data, error } = await inviteParticipant({ + path: { + abbreviation: formData.tournamentAbbreviation, + teamId: formData.teamId, + osuId: formData.osuId, + }, + }); + + if (error) { + return { + status: false as const, + errorMessage: error.errors?.map((e) => e).join(", "), + }; + } + + await multipleRevalidatePaths([ + "/", + `/tournament/${formData.tournamentAbbreviation}/teams/${formData.teamId}`, + `/tournament/${formData.tournamentAbbreviation}`, + ]); - return executeFetch( - TeamService.inviteParticipant(data.tournamentAbbreviation, data.teamId, data.osuId), - [ - "/", - `/tournament/${data.tournamentAbbreviation}/teams/${data.teamId}`, - `/tournament/${data.tournamentAbbreviation}`, - ], - ) - .then((res) => { - return res as SuccessfulResponse; - }) - .catch((error) => { - return error as ErrorResponse; - }); + return { + status: true as const, + response: data, + }; } diff --git a/src/actions/public/createTournamentAction.ts b/src/actions/public/createTournamentAction.ts index df22e53..6ed8292 100644 --- a/src/actions/public/createTournamentAction.ts +++ b/src/actions/public/createTournamentAction.ts @@ -1,29 +1,51 @@ "use server"; +import { cookies } from "next/headers"; import { - AdminTournamentService, - type TournamentRequestDto, - type TournamentResponseDto, -} from "../../../generated"; + client, + createTournament, + type qualificationType, + type tournamentType, +} from "../../../client"; import { type CreateTournamentSchemaType } from "@/formSchemas/createTournamentSchema"; -import { type ErrorResponse, executeFetch, type SuccessfulResponse } from "@/lib/executeFetch"; +import { multipleRevalidatePaths } from "@/lib/multipleRevalidatePaths"; -export async function createTournamentAction(data: CreateTournamentSchemaType) { +export async function createTournamentAction(formData: CreateTournamentSchemaType) { "use server"; + const cookie = cookies().get("JWT")?.value; + // configure internal service client + client.setConfig({ + // set default base url for requests + baseUrl: process.env.NEXT_PUBLIC_API_URL, + // set default headers for requests + headers: { + Cookie: `token=${cookie}`, + }, + }); + const { data, error } = await createTournament({ + body: { + name: formData.name, + abbreviation: formData.abbreviation, + tournamentType: formData.tournamentType as tournamentType, + qualificationType: formData.qualificationType as qualificationType, + minimumRankLimit: +formData.minimumRankLimit, + maximumRankLimit: +formData.maximumRankLimit, + minimumTeamSize: +formData.minimumTeamSize, + maximumTeamSize: +formData.maximumTeamSize, + }, + }); - return executeFetch( - AdminTournamentService.createTournament({ - name: data.name, - abbreviation: data.abbreviation, - tournamentType: data.tournamentType as TournamentRequestDto.tournamentType, - qualificationType: data.qualificationType as TournamentRequestDto.qualificationType, - minimumRankLimit: +data.minimumRankLimit, - maximumRankLimit: +data.maximumRankLimit, - minimumTeamSize: +data.minimumTeamSize, - maximumTeamSize: +data.maximumTeamSize, - }), - ["/", "/dashboard"], - ) - .then((res) => res as SuccessfulResponse) - .catch((error) => error as ErrorResponse); + if (error) { + return { + status: false as const, + errorMessage: error.errors?.map((e) => e).join(", "), + }; + } + + await multipleRevalidatePaths(["/", "/dashboard"]); + + return { + status: true as const, + response: data, + }; } diff --git a/src/actions/public/decideRescheduleAction.ts b/src/actions/public/decideRescheduleAction.ts new file mode 100644 index 0000000..3c9020c --- /dev/null +++ b/src/actions/public/decideRescheduleAction.ts @@ -0,0 +1,49 @@ +"use server"; + +import { cookies } from "next/headers"; +import { client, decideMatchReschedule } from "../../../client"; +import { multipleRevalidatePaths } from "@/lib/multipleRevalidatePaths"; + +export async function decideRescheduleAction( + tournamentAbbreviation: string, + matchId: string, + isAccepted: boolean, +) { + "use server"; + const cookie = cookies().get("JWT")?.value; + client.setConfig({ + baseUrl: process.env.NEXT_PUBLIC_API_URL, + }); + const { data, error } = await decideMatchReschedule({ + path: { + abbreviation: tournamentAbbreviation, + }, + body: { + matchId: matchId, + isAccepted: isAccepted, + }, + headers: { + Cookie: `token=${cookie}`, + }, + }); + + if (error) { + return { + status: false as const, + errorMessage: error.errors?.map((e) => e).join(", ") || "Failed to decide on reschedule", + }; + } + + await multipleRevalidatePaths([ + "/", + `/dashboard/${tournamentAbbreviation}/matches`, + `/tournament/${tournamentAbbreviation}/schedule`, + ]); + + return { + status: true as const, + response: data, + }; +} + + diff --git a/src/actions/public/deleteParticipantFromTeamAction.ts b/src/actions/public/deleteParticipantFromTeamAction.ts new file mode 100644 index 0000000..f8ed3a7 --- /dev/null +++ b/src/actions/public/deleteParticipantFromTeamAction.ts @@ -0,0 +1,47 @@ +"use server"; + +import { cookies } from "next/headers"; +import { client, deleteParticipantFromTeam } from "../../../client"; +import { multipleRevalidatePaths } from "@/lib/multipleRevalidatePaths"; + +export async function deleteParticipantFromTeamAction( + tournamentAbbreviation: string, + teamId: string, + participantId: string, +) { + "use server"; + const cookie = cookies().get("JWT")?.value; + client.setConfig({ + baseUrl: process.env.NEXT_PUBLIC_API_URL, + headers: { + Cookie: `token=${cookie}`, + }, + }); + + const { error } = await deleteParticipantFromTeam({ + path: { + abbreviation: tournamentAbbreviation, + teamId: teamId, + participantId: participantId, + }, + }); + + if (error) { + return { + status: false as const, + errorMessage: error.errors?.map((e) => e).join(", ") || "Failed to remove participant", + }; + } + + await multipleRevalidatePaths([ + `/tournament/${tournamentAbbreviation}/teams/${teamId}`, + `/tournament/${tournamentAbbreviation}/teams`, + `/tournament/${tournamentAbbreviation}`, + "/", + ]); + + return { + status: true as const, + }; +} + diff --git a/src/actions/public/disbandTeamAction.ts b/src/actions/public/disbandTeamAction.ts new file mode 100644 index 0000000..a17c951 --- /dev/null +++ b/src/actions/public/disbandTeamAction.ts @@ -0,0 +1,44 @@ +"use server"; + +import { cookies } from "next/headers"; +import { client, disbandTeam } from "../../../client"; +import { multipleRevalidatePaths } from "@/lib/multipleRevalidatePaths"; + +export async function disbandTeamAction( + tournamentAbbreviation: string, + teamId: string, +) { + "use server"; + const cookie = cookies().get("JWT")?.value; + client.setConfig({ + baseUrl: process.env.NEXT_PUBLIC_API_URL, + headers: { + Cookie: `token=${cookie}`, + }, + }); + + const { error } = await disbandTeam({ + path: { + abbreviation: tournamentAbbreviation, + teamId: teamId, + }, + }); + + if (error) { + return { + status: false as const, + errorMessage: error.errors?.map((e) => e).join(", ") || "Failed to disband team", + }; + } + + await multipleRevalidatePaths([ + `/tournament/${tournamentAbbreviation}/teams`, + `/tournament/${tournamentAbbreviation}`, + "/", + ]); + + return { + status: true as const, + }; +} + diff --git a/src/actions/public/getStaffMembersAction.ts b/src/actions/public/getStaffMembersAction.ts new file mode 100644 index 0000000..f7bdea4 --- /dev/null +++ b/src/actions/public/getStaffMembersAction.ts @@ -0,0 +1,39 @@ +"use server"; + +import { cookies } from "next/headers"; +import { client, getStaffMembers1 } from "../../../client"; +import type { selectOptions } from "@ui/atoms/Forms/Select/ComboBox"; + +export async function getStaffMembersAction( + tournamentAbbreviation: string, +): Promise { + const cookie = cookies().get("JWT")?.value; + client.setConfig({ + baseUrl: process.env.NEXT_PUBLIC_API_URL, + headers: { + Cookie: `token=${cookie}`, + }, + }); + + const { data } = await getStaffMembers1({ + path: { + abbreviation: tournamentAbbreviation, + }, + }); + + const staffMemberOptions: selectOptions[] = + data + ?.filter((s) => s.user) + ?.map((staffMember) => ({ + id: staffMember.id, + label: staffMember.user + ? staffMember.user.username + : staffMember.username || "", + })) || []; + + return staffMemberOptions; +} + + + + diff --git a/src/actions/public/getTournamentAction.ts b/src/actions/public/getTournamentAction.ts new file mode 100644 index 0000000..b53ce88 --- /dev/null +++ b/src/actions/public/getTournamentAction.ts @@ -0,0 +1,39 @@ +"use server"; + +import { cookies } from "next/headers"; +import { client, getTournamentByAbbreviation, type TournamentResponseDto } from "../../../client"; + +export async function getTournamentAction( + abbreviation: string, +): Promise<{ status: boolean; data?: TournamentResponseDto; errorMessage?: string }> { + "use server"; + const cookie = cookies().get("JWT")?.value; + // configure internal service client + client.setConfig({ + // set default base url for requests + baseUrl: process.env.NEXT_PUBLIC_API_URL, + // set default headers for requests + headers: { + Cookie: `token=${cookie}`, + }, + }); + + const { data, error } = await getTournamentByAbbreviation({ + path: { + abbreviation, + }, + }); + + if (error) { + return { + status: false, + errorMessage: error.errors?.map((e) => e).join(", "), + }; + } + + return { + status: true, + data, + }; +} + diff --git a/src/actions/public/getUserAction.ts b/src/actions/public/getUserAction.ts index 978bff0..29291c0 100644 --- a/src/actions/public/getUserAction.ts +++ b/src/actions/public/getUserAction.ts @@ -1,15 +1,21 @@ import { cache } from "react"; -import { OpenAPI, UserService } from "../../../generated"; -import { verifySession } from "@/lib/session"; +import { cookies } from "next/headers"; +import { client, me } from "../../../client"; export const getUser = cache(async () => { - const JWT = await verifySession(); - if (typeof JWT.token === "string" && JWT.isAuth) { - OpenAPI.HEADERS = { - Cookie: `token=${JWT.token}`, - }; - const userData = await UserService.me(); - return userData; + const cookie = cookies().get("JWT")?.value; + // configure internal service client + client.setConfig({ + // set default base url for requests + baseUrl: process.env.NEXT_PUBLIC_API_URL, + // set default headers for requests + headers: { + Cookie: `token=${cookie}`, + }, + }); + if (cookie) { + const { data } = await me(); + return data; } return null; }); diff --git a/src/actions/public/participianJoinToTheTournamentAction.ts b/src/actions/public/participianJoinToTheTournamentAction.ts index 44f7b37..3b3d15e 100644 --- a/src/actions/public/participianJoinToTheTournamentAction.ts +++ b/src/actions/public/participianJoinToTheTournamentAction.ts @@ -1,22 +1,24 @@ -"use server"; +// "use server"; +// +// import { type ParticipantResponseDto, ParticipantService } from "../../../generated"; +// import { type ErrorResponse, executeFetch, type SuccessfulResponse } from "@/lib/executeFetch"; +// +// export async function participianJoinToTheTournamentAction(data: FormData) { +// "use server"; +// const tournamentAbb = data.get("tournamentAbb") as string; +// +// return executeFetch(ParticipantService.registerParticipant(tournamentAbb), [ +// "/", +// "/dashboard", +// "/tournament", +// "/registration", +// ]) +// .then((res) => { +// return res as SuccessfulResponse; +// }) +// .catch((error) => { +// return error as ErrorResponse; +// }); +// } -import { type ParticipantResponseDto, ParticipantService } from "../../../generated"; -import { type ErrorResponse, executeFetch, type SuccessfulResponse } from "@/lib/executeFetch"; - -export async function participianJoinToTheTournamentAction(data: FormData) { - "use server"; - const tournamentAbb = data.get("tournamentAbb") as string; - - return executeFetch(ParticipantService.registerParticipant(tournamentAbb), [ - "/", - "/dashboard", - "/tournament", - "/registration", - ]) - .then((res) => { - return res as SuccessfulResponse; - }) - .catch((error) => { - return error as ErrorResponse; - }); -} +//todo diff --git a/src/actions/public/rescheduleMatchAction.ts b/src/actions/public/rescheduleMatchAction.ts new file mode 100644 index 0000000..d7668f4 --- /dev/null +++ b/src/actions/public/rescheduleMatchAction.ts @@ -0,0 +1,46 @@ +"use server"; + +import { cookies } from "next/headers"; +import { client, rescheduleMatch } from "../../../client"; +import { multipleRevalidatePaths } from "@/lib/multipleRevalidatePaths"; +import { type RescheduleMatchSchemaType } from "@/formSchemas/rescheduleMatchSchema"; + +export async function rescheduleMatchAction(formData: RescheduleMatchSchemaType) { + "use server"; + const cookie = cookies().get("JWT")?.value; + client.setConfig({ + baseUrl: process.env.NEXT_PUBLIC_API_URL, + }); + const { data, error } = await rescheduleMatch({ + path: { + abbreviation: formData.tournamentAbbreviation, + }, + body: { + matchId: formData.matchId, + proposedDateTime: formData.proposedDateTime, + }, + headers: { + Cookie: `token=${cookie}`, + }, + }); + + if (error) { + return { + status: false as const, + errorMessage: error.errors?.map((e) => e).join(", ") || "Failed to reschedule match", + }; + } + + await multipleRevalidatePaths([ + "/", + `/dashboard/${formData.tournamentAbbreviation}/matches`, + `/tournament/${formData.tournamentAbbreviation}/schedule`, + ]); + + return { + status: true as const, + response: data, + }; +} + + diff --git a/src/app/(main-page)/(withAuth)/account/create-team/page.tsx b/src/app/(main-page)/(withAuth)/account/create-team/page.tsx deleted file mode 100644 index 7b1c2db..0000000 --- a/src/app/(main-page)/(withAuth)/account/create-team/page.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { Button } from "@ui/atoms/Button/Button"; - -const CreateTeamPage = async ({ - params, -}: { - params: { - tournamentId: string; - }; -}) => { - return ( - <> -
-
-
-
-

- Create a team -

-

- Enter your team name -

- - -
-
- -
-
- - ); -}; - -export default CreateTeamPage; diff --git a/src/app/(main-page)/(withAuth)/account/page.tsx b/src/app/(main-page)/(withAuth)/account/page.tsx index 04b8ba6..c9ef8a1 100644 --- a/src/app/(main-page)/(withAuth)/account/page.tsx +++ b/src/app/(main-page)/(withAuth)/account/page.tsx @@ -7,8 +7,10 @@ import UserTeams from "@ui/organisms/UserInformation/UserTeams"; const AccountPage = async () => { return ( -
- +
+
+ +
}>
@@ -20,11 +22,9 @@ const AccountPage = async () => {
}> -
-
-

My teams

- -
+
+

My teams

+
{/*}>*/} @@ -37,7 +37,7 @@ const AccountPage = async () => { {/*
*/} {/*
*/} {/**/} - + ); }; diff --git a/src/app/(main-page)/(withAuth)/dashboard/CreateTournamentModal.tsx b/src/app/(main-page)/(withAuth)/dashboard/CreateTournamentModal.tsx index 09ef3ef..2f431af 100644 --- a/src/app/(main-page)/(withAuth)/dashboard/CreateTournamentModal.tsx +++ b/src/app/(main-page)/(withAuth)/dashboard/CreateTournamentModal.tsx @@ -4,7 +4,7 @@ import React, { useRef } from "react"; import { IoMdAdd } from "react-icons/io"; import { useRouter } from "next/navigation"; import { toast } from "sonner"; -import { TournamentRequestDto } from "../../../../../generated"; +import { qualificationType, tournamentType } from '../../../../../client' import { Button } from "@ui/atoms/Button/Button"; import Modal from "@ui/organisms/Modal/Modal"; import { useTypeSafeFormState } from "@/hooks/useTypeSafeFormState"; @@ -30,20 +30,20 @@ const CreateTournamentModal = () => { router.push(`/dashboard/${tournamentResponseDto.response.abbreviation}`); }); const [tournamentTourType, setTournamentTourType] = - React.useState( - TournamentRequestDto.tournamentType.TEAM_VS, + React.useState( + tournamentType.TEAM_VS, ); const tournamentTourTypeSelectOptions: selectOptions[] = [ - { id: TournamentRequestDto.tournamentType.TEAM_VS, label: "Team vs" }, - { id: TournamentRequestDto.tournamentType.INTERNATIONAL, label: "International" }, + { id: tournamentType.TEAM_VS, label: "Team vs" }, + { id: tournamentType.INTERNATIONAL, label: "International" }, // todo // { id: TournamentRequestDto.tournamentType.PARTICIPANT_VS, label: "Participant vs" }, ]; const qualificationTypeSelectOptions: selectOptions[] = [ - { id: TournamentRequestDto.qualificationType.Z_SUM, label: "Z-sum" }, - { id: TournamentRequestDto.qualificationType.ZIP_LAW, label: "Zip law" }, + { id: qualificationType.Z_SUM, label: "Z-sum" }, + { id: qualificationType.ZIP_LAW, label: "Zip law" }, ]; return ( @@ -86,7 +86,7 @@ const CreateTournamentModal = () => { selectOptions={tournamentTourTypeSelectOptions} onSelect={(e) => { setTournamentTourType( - e.target.value as TournamentRequestDto.tournamentType, + e.target.value as tournamentType, ); }} /> @@ -114,7 +114,7 @@ const CreateTournamentModal = () => { label={"Maximum rank limit"} /> {tournamentTourType !== - TournamentRequestDto.tournamentType.PARTICIPANT_VS && ( + tournamentType.PARTICIPANT_VS && ( <> {tournamentData.errorMessage}; - } + const cookie = cookies().get("JWT")?.value; + // configure internal service client + client.setConfig({ + // set default base url for requests + baseUrl: process.env.NEXT_PUBLIC_API_URL, + // set default headers for requests + headers: { + Cookie: `token=${cookie}`, + }, + }); + const { data: tournamentData } = await getTournamentByAbbreviation({ + path: { + abbreviation: tournamentAbbreviation, + }, + }); return ( <> @@ -48,7 +55,7 @@ export default async function Layout({
  • Matches
  • - {tournamentData?.response.tournamentType !== tournamentType.PARTICIPANT_VS ? ( + {tournamentData?.tournamentType !== tournamentType.PARTICIPANT_VS ? (
  • Teams
  • diff --git a/src/app/(main-page)/(withAuth)/dashboard/[tournamentAbbreviation]/mappool/[stageType]/[mappoolId]/AddBeatMap.tsx b/src/app/(main-page)/(withAuth)/dashboard/[tournamentAbbreviation]/mappool/[stageType]/[mappoolId]/AddBeatMap.tsx index 97089aa..898167e 100644 --- a/src/app/(main-page)/(withAuth)/dashboard/[tournamentAbbreviation]/mappool/[stageType]/[mappoolId]/AddBeatMap.tsx +++ b/src/app/(main-page)/(withAuth)/dashboard/[tournamentAbbreviation]/mappool/[stageType]/[mappoolId]/AddBeatMap.tsx @@ -1,7 +1,8 @@ "use client"; import React, { useRef } from "react"; +import { useRouter } from "next/navigation"; import { toast } from "sonner"; -import { BeatmapModificationResponseDto } from "../../../../../../../../../generated"; +import { modification, type stageType } from "../../../../../../../../../client"; import { Button } from "@ui/atoms/Button/Button"; import Modal from "@ui/organisms/Modal/Modal"; import { ComboBox } from "@ui/atoms/Forms/Select/ComboBox"; @@ -14,69 +15,84 @@ import { addBeatMapAction } from "@/actions/admin/adminBeatMapActions"; interface IAddStageFormProps { tournamentAbb: string; mappoolId: string; + stageType: stageType; } -export const AddBeatMap = ({ tournamentAbb, mappoolId }: IAddStageFormProps) => { +export const AddBeatMap = ({ tournamentAbb, mappoolId, stageType }: IAddStageFormProps) => { const modalRef = useRef(null); const formRef = React.useRef(null); + const router = useRouter(); const [state, formAction] = useTypeSafeFormState(addBeatMapSchema, async (data) => { - const addBeatMapResponse = await addBeatMapAction(data); + const addBeatMapResponse = await addBeatMapAction(data, stageType); if (!addBeatMapResponse.status) { return toast.error(addBeatMapResponse.errorMessage, { duration: 3000, }); } + toast.success("Beatmap added successfully", { + duration: 3000, + }); modalRef.current?.close(); resetFormValues({ formRef, resetWithoutInputNames: ["tournamentAbb", "mappoolId"], schema: addBeatMapSchema, }); + router.refresh(); }); + const handleOpenModal = () => { + resetFormValues({ + formRef, + resetWithoutInputNames: ["tournamentAbb", "mappoolId"], + schema: addBeatMapSchema, + }); + modalRef?.current?.showModal(); + }; + const modificationTypeSelectOptions = [ { - id: BeatmapModificationResponseDto.modification.DT, + id: modification.DT, label: "DT", }, { - id: BeatmapModificationResponseDto.modification.HR, + id: modification.HR, label: "HR", }, { - id: BeatmapModificationResponseDto.modification.HD, + id: modification.HD, label: "HD", }, { - id: BeatmapModificationResponseDto.modification.FL, + id: modification.FL, label: "FL", }, { - id: BeatmapModificationResponseDto.modification.EZ, + id: modification.EZ, label: "EZ", }, { - id: BeatmapModificationResponseDto.modification.NM, + id: modification.NM, label: "NM", }, { - id: BeatmapModificationResponseDto.modification.TB, + id: modification.TB, label: "TB", }, { - id: BeatmapModificationResponseDto.modification.FM, + id: modification.FM, label: "FM", }, { - id: BeatmapModificationResponseDto.modification.HT, + id: modification.HT, label: "HT", }, ]; return ( <> - diff --git a/src/app/(main-page)/(withAuth)/dashboard/[tournamentAbbreviation]/mappool/[stageType]/[mappoolId]/page.tsx b/src/app/(main-page)/(withAuth)/dashboard/[tournamentAbbreviation]/mappool/[stageType]/[mappoolId]/page.tsx index f6f6bca..1f18a2e 100644 --- a/src/app/(main-page)/(withAuth)/dashboard/[tournamentAbbreviation]/mappool/[stageType]/[mappoolId]/page.tsx +++ b/src/app/(main-page)/(withAuth)/dashboard/[tournamentAbbreviation]/mappool/[stageType]/[mappoolId]/page.tsx @@ -1,26 +1,36 @@ import React from "react"; import Image from "next/image"; -import { AdminMappoolService, type StageResponseDto } from "../../../../../../../../../generated"; +import { cookies } from "next/headers"; +import { + client, + deleteBeatmap, + getMappool, + releaseMappool, + type stageType, +} from "../../../../../../../../../client"; import { stageTypeEnumToString } from "@/lib/helpers"; import { Button } from "@ui/atoms/Button/Button"; import { AddBeatMap } from "@/app/(main-page)/(withAuth)/dashboard/[tournamentAbbreviation]/mappool/[stageType]/[mappoolId]/AddBeatMap"; -import { executeFetch } from "@/lib/executeFetch"; +import { multipleRevalidatePaths } from "@/lib/multipleRevalidatePaths"; const StageTypePage = async ({ params: { tournamentAbbreviation, stageType, mappoolId }, }: { params: { tournamentAbbreviation: string; - stageType: StageResponseDto.stageType; + stageType: stageType; mappoolId: string; }; }) => { - const getMappoolData = await executeFetch( - AdminMappoolService.getMappool(tournamentAbbreviation, stageType), - ); + const { data: getMappoolData, error } = await getMappool({ + path: { + stageType, + abbreviation: tournamentAbbreviation, + }, + }); - if (!getMappoolData.status) { - return
    {getMappoolData.errorMessage}
    ; + if (error) { + return
    {error.errors?.map((e) => e)}
    ; } return ( @@ -32,21 +42,37 @@ const StageTypePage = async ({ className={"mb-3"} action={async (_e) => { "use server"; - await executeFetch( - AdminMappoolService.releaseMappool( - tournamentAbbreviation, + const cookie = cookies().get("JWT")?.value; + // configure internal service client + client.setConfig({ + // set default base url for requests + baseUrl: process.env.NEXT_PUBLIC_API_URL, + // set default headers for requests + headers: { + Cookie: `token=${cookie}`, + }, + }); + await releaseMappool({ + path: { stageType, - !getMappoolData.response.isReleased, - ), - ["/", "/dashboard/[tournamentAbb]/mappool/[stageType]/[mappoolId]"], - ); + abbreviation: tournamentAbbreviation, + }, + query: { + release: !getMappoolData.isReleased, + }, + }); + + await multipleRevalidatePaths([ + "/", + `/dashboard/${tournamentAbbreviation}/mappool/${stageType}/${mappoolId}`, + ]); }} > - +
    {/* head */} @@ -69,7 +95,7 @@ const StageTypePage = async ({ - {getMappoolData.response.beatmapsModifications.map( + {getMappoolData.beatmapsModifications.map( (mappool) => mappool.beatmaps?.map((beatmap) => ( @@ -94,7 +120,7 @@ const StageTypePage = async ({ - + - + - {qualificationRooms(getQualificationRooms.response).map((room) => ( + {qualificationRooms(getQualificationRoomsData || []).map((room) => ( - - + - {getStaffMembers.response.map((s) => ( + {getStaffMembers?.map((s) => ( - {getStagesData.response - .sort((a, b) => { + {getStagesData + ?.sort((a, b) => { return ( new Date(a.startDate).getTime() - new Date(b.startDate).getTime() @@ -89,13 +87,26 @@ const StagePage = async ({ { "use server"; - await executeFetch( - AdminStageService.deleteStage( - tournamentAbbreviation, - stage.stageType, - ), - ["/", "/dashboard/[tournamentAbb]/stages"], - ); + const cookie = cookies().get("JWT")?.value; + // configure internal service client + client.setConfig({ + // set default base url for requests + baseUrl: process.env.NEXT_PUBLIC_API_URL, + // set default headers for requests + headers: { + Cookie: `token=${cookie}`, + }, + }); + await deleteStage({ + path: { + abbreviation: tournamentAbbreviation, + stageType: stage.stageType, + }, + }); + await multipleRevalidatePaths([ + "/", + `/dashboard/${tournamentAbbreviation}/stages`, + ]); }} > + + +

    + {showConfirm ? "Confirm Deletion" : "Delete Team"} +

    +
    + {!showConfirm ? ( + <> +

    + Are you sure you want to delete the team "{teamName}"? +

    +
    +

    Warning:

    +

    + This action will permanently delete all associated tournament data + including: +

    +
      +
    • All matches involving this team
    • +
    • All match results
    • +
    • All participants in this team
    • +
    • All related tournament data
    • +
    +

    + This action cannot be undone. +

    +
    +
    + + +
    + + ) : ( + <> +

    + Are you absolutely sure you want to delete the team{" "} + "{teamName}"? +

    +

    + This will permanently delete all associated data. This action cannot be + undone. +

    +
    + + +
    + + )} +
    +
    + + ); +}; + diff --git a/src/app/(main-page)/(withAuth)/dashboard/[tournamentAbbreviation]/teams/EditTeamModal.tsx b/src/app/(main-page)/(withAuth)/dashboard/[tournamentAbbreviation]/teams/EditTeamModal.tsx new file mode 100644 index 0000000..d179104 --- /dev/null +++ b/src/app/(main-page)/(withAuth)/dashboard/[tournamentAbbreviation]/teams/EditTeamModal.tsx @@ -0,0 +1,455 @@ +"use client"; + +import React, { useRef, useState, useMemo } from "react"; +import { toast } from "sonner"; +import Image from "next/image"; +import { useRouter } from "next/navigation"; +import type { TeamResponseDto, ParticipantResponseDto } from "../../../../../../../client"; +import { RemoveParticipantModal } from "./RemoveParticipantModal"; +import { Button } from "@ui/atoms/Button/Button"; +import Modal from "@ui/organisms/Modal/Modal"; +import { Input } from "@ui/atoms/Forms/Input/Input"; +import { useTypeSafeFormState } from "@/hooks/useTypeSafeFormState"; +import { updateTeamSchema } from "@/formSchemas/updateTeamSchema"; +import { + updateTeamAction, + addParticipantAction, + removeParticipantAction, + changeCaptainAction, +} from "@/actions/admin/adminTeamActions"; + +interface IEditTeamModalProps { + tournamentAbb: string; + team: TeamResponseDto; +} + +export const EditTeamModal = ({ tournamentAbb, team }: IEditTeamModalProps) => { + const modalRef = useRef(null); + const formRef = useRef(null); + const router = useRouter(); + const [participants, setParticipants] = useState(team.participants); + const [captain, setCaptain] = useState(team.captain); + const [osuIdToAdd, setOsuIdToAdd] = useState(""); + const [isAddingParticipant, setIsAddingParticipant] = useState(false); + const [isRemovingParticipant, setIsRemovingParticipant] = useState(null); + const [isChangingCaptain, setIsChangingCaptain] = useState(null); + const [selectedLogo, setSelectedLogo] = useState(null); + const [logoPreview, setLogoPreview] = useState(null); + const [logoError, setLogoError] = useState(null); + + // Create refs for remove participant modals + const removeModalRefs = useMemo(() => { + const refs: Record> = {}; + participants.forEach((participant) => { + if (participant.id !== captain.id) { + refs[participant.id] = React.createRef(); + } + }); + return refs; + }, [participants, captain.id]); + + const validateLogo = (file: File): Promise => { + return new Promise((resolve) => { + // Check file size (max 2MB) + if (file.size > 2 * 1024 * 1024) { + resolve("Image size cannot exceed 2MB"); + return; + } + + // Check file extension + const allowedExtensions = [".jpg", ".jpeg", ".png"]; + const fileName = file.name.toLowerCase(); + const hasValidExtension = allowedExtensions.some(ext => fileName.endsWith(ext)); + if (!hasValidExtension) { + resolve("Image must be in format: jpg, jpeg, or png"); + return; + } + + // Check image dimensions + const img = new window.Image(); + const objectUrl = URL.createObjectURL(file); + img.onload = () => { + URL.revokeObjectURL(objectUrl); + if (img.width > 50 || img.height > 50) { + resolve("Image dimensions cannot exceed 50x50 pixels"); + } else { + resolve(null); + } + }; + img.onerror = () => { + URL.revokeObjectURL(objectUrl); + resolve("Invalid image file"); + }; + img.src = objectUrl; + }); + }; + + const handleLogoChange = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) { + setSelectedLogo(null); + setLogoPreview(null); + setLogoError(null); + return; + } + + setLogoError(null); + + // Validate file + const error = await validateLogo(file); + if (error) { + setLogoError(error); + setSelectedLogo(null); + setLogoPreview(null); + e.target.value = ""; + return; + } + + setSelectedLogo(file); + + // Create preview + const reader = new FileReader(); + reader.onloadend = () => { + setLogoPreview(reader.result as string); + }; + reader.readAsDataURL(file); + }; + + const [state, formAction] = useTypeSafeFormState(updateTeamSchema, async (data) => { + // Create FormData from form data and logo file + const formDataToSend = new FormData(); + formDataToSend.append("tournamentAbbreviation", data.tournamentAbbreviation); + formDataToSend.append("teamId", data.teamId); + formDataToSend.append("name", data.name); + if (selectedLogo) { + formDataToSend.append("logo", selectedLogo); + } + + const updateTeamResponse = await updateTeamAction(formDataToSend); + if (!updateTeamResponse.status) { + const errorMsg: string = updateTeamResponse.errorMessage || "Failed to update team"; + return toast.error(errorMsg, { + duration: 3000, + }); + } + toast.success("Team updated successfully", { + duration: 3000, + }); + setSelectedLogo(null); + setLogoPreview(null); + setLogoError(null); + modalRef.current?.close(); + router.refresh(); + }); + + const handleAddParticipant = async () => { + if (!osuIdToAdd.trim()) { + toast.error("Please enter an osu! ID", { + duration: 3000, + }); + return; + } + + setIsAddingParticipant(true); + const result = await addParticipantAction(tournamentAbb, team.id, osuIdToAdd.trim()); + setIsAddingParticipant(false); + + if (!result.status) { + const errorMsg: string = result.errorMessage || "Failed to add participant"; + toast.error(errorMsg, { + duration: 3000, + }); + return; + } + + // Update local state with returned team data + if (result.team) { + setParticipants(result.team.participants || []); + if (result.team.captain) { + setCaptain(result.team.captain); + } + } + + toast.success("Participant added successfully", { + duration: 3000, + }); + setOsuIdToAdd(""); + router.refresh(); + }; + + const handleRemoveParticipant = async (participant: ParticipantResponseDto) => { + if (participant.id === captain.id) { + toast.error("Cannot remove team captain. Change captain first.", { + duration: 3000, + }); + return; + } + + setIsRemovingParticipant(participant.id); + const result = await removeParticipantAction( + tournamentAbb, + team.id, + participant.user.osuId.toString(), + ); + setIsRemovingParticipant(null); + + if (!result.status) { + const errorMsg: string = result.errorMessage || "Failed to remove participant"; + toast.error(errorMsg, { + duration: 3000, + }); + return; + } + + // Update local state with returned team data + if (result.team) { + setParticipants(result.team.participants || []); + if (result.team.captain) { + setCaptain(result.team.captain); + } + } + + toast.success("Participant removed successfully", { + duration: 3000, + }); + router.refresh(); + }; + + const handleChangeCaptain = async (participant: ParticipantResponseDto) => { + if (participant.id === captain.id) { + return; + } + + setIsChangingCaptain(participant.id); + const result = await changeCaptainAction( + tournamentAbb, + team.id, + participant.user.osuId.toString(), + ); + setIsChangingCaptain(null); + + if (!result.status) { + const errorMsg: string = result.errorMessage || "Failed to change captain"; + toast.error(errorMsg, { + duration: 3000, + }); + return; + } + + // Update local state with returned team data + if (result.team) { + setParticipants(result.team.participants || []); + if (result.team.captain) { + setCaptain(result.team.captain); + } + } + + toast.success("Captain changed successfully", { + duration: 3000, + }); + router.refresh(); + }; + + return ( + <> + + + +

    Edit Team: {team.name}

    +
    e.stopPropagation()}> +
    + + + + {/* Team Name */} + + + {/* Team Logo */} +
    + + + {logoError && ( +
    {logoError}
    + )} + {(logoPreview || (team.logoUrl && !selectedLogo && team.logoUrl !== "/aim_logo.svg")) && ( +
    + Preview: +
    +
    + +
    +
    +
    + )} +
    + + {/* Participants Management */} +
    Participants
    + + {/* Add Participant */} +
    + setOsuIdToAdd(e.target.value)} + placeholder="Enter osu! ID" + /> + +
    + + {/* Participants Table */} +
    +
    {beatmap.beatmapStatistics.ar.toFixed(2)} {beatmap.beatmapStatistics.hp.toFixed(2)} {beatmap.beatmapStatistics.starRating.toFixed(2)}{beatmap.beatmapStatistics.bpm}{beatmap.beatmapStatistics.bpm.toFixed(2)} {beatmap.beatmapStatistics.length} {beatmap.creator} @@ -111,18 +137,18 @@ const StageTypePage = async ({
    { "use server"; - await executeFetch( - AdminMappoolService.deleteBeatmap( - tournamentAbbreviation, + await deleteBeatmap({ + path: { + abbreviation: tournamentAbbreviation, mappoolId, - mappool.modification, - beatmap.id, - ), - [ - "/", - "/dashboard/[tournamentAbb]/mappool/[stageType]/[mappoolId]", - ], - ); + modification: mappool.modification, + beatmapId: beatmap.id, + }, + }); + await multipleRevalidatePaths([ + "/", + `/dashboard/${tournamentAbbreviation}/mappool/${stageType}/${mappoolId}`, + ]); }} > + + +

    Edit match

    + +
    + + + + + referee.id) || [] + } + errorMessage={ + stateEditMatch?.errors.refereeIds && + stateEditMatch?.errors.refereeIds[0] + } + /> + + commentator.id) || [] + } + errorMessage={ + stateEditMatch?.errors.commentatorIds && + stateEditMatch?.errors.commentatorIds[0] + } + /> + + streamer.id) || [] + } + errorMessage={ + stateEditMatch?.errors.streamerIds && + stateEditMatch?.errors.streamerIds[0] + } + /> +
    + + +
    + + ); +}; + diff --git a/src/app/(main-page)/(withAuth)/dashboard/[tournamentAbbreviation]/matches/MatchesModal.tsx b/src/app/(main-page)/(withAuth)/dashboard/[tournamentAbbreviation]/matches/MatchesModal.tsx new file mode 100644 index 0000000..e7bab7f --- /dev/null +++ b/src/app/(main-page)/(withAuth)/dashboard/[tournamentAbbreviation]/matches/MatchesModal.tsx @@ -0,0 +1,280 @@ +"use client"; +import React, { useRef } from "react"; +import { toast } from "sonner"; +import { stageType } from "../../../../../../../client"; +import { Button } from "@ui/atoms/Button/Button"; +import Modal from "@ui/organisms/Modal/Modal"; +import { ComboBox, type selectOptions } from "@ui/atoms/Forms/Select/ComboBox"; +import { StaffMemberAutocomplete } from "@ui/atoms/Forms/Select/StaffMemberAutocomplete"; +import { Input } from "@ui/atoms/Forms/Input/Input"; +import { useTypeSafeFormState } from "@/hooks/useTypeSafeFormState"; +import { editQualificationRoomsAction } from "@/actions/admin/adminQualificationRoomsActions"; +import { resetFormValues, stageTypeEnumToString } from "@/lib/helpers"; +import { editQualificationRoomsSchema } from "@/formSchemas/editQualificationRoomSchema"; +import { addMatchSchema } from "@/formSchemas/addMachSchema"; +import { addMatchAction } from "@/actions/admin/adminAddMatchActions"; + +type AddNewQRoom = { + type: "add"; +}; + +type EditQRoom = { + type: "edit"; + room: { + roomId: string; + dataTimeStart: string; + selectedStaffMemberOption: selectOptions[]; + selectedRosterIds: selectOptions[]; + }; + staffMemberSelectOptions: selectOptions[]; + rostersSelectOptions: selectOptions[]; +}; + +type QRoomModalType = AddNewQRoom | EditQRoom; + +interface IAddStaffMemberProps { + tournamentAbb: string; + modalType: QRoomModalType; + teams: selectOptions[]; + staffMembers: selectOptions[]; +} + +export const MatchesModal = ({ + tournamentAbb, + modalType, + teams, + staffMembers, +}: IAddStaffMemberProps) => { + const modalRef = useRef(null); + const formRef = React.useRef(null); + + const stageTypeSelectOptions: selectOptions[] = [ + { + id: stageType.FINAL, + label: stageTypeEnumToString(stageType.FINAL), + }, + { + id: stageType.RO16, + label: stageTypeEnumToString(stageType.RO16), + }, + { + id: stageType.RO64, + label: stageTypeEnumToString(stageType.RO64), + }, + { + id: stageType.GRAND_FINAL, + label: stageTypeEnumToString(stageType.GRAND_FINAL), + }, + { + id: stageType.QUARTER_FINAL, + label: stageTypeEnumToString(stageType.QUARTER_FINAL), + }, + { + id: stageType.RO32, + label: stageTypeEnumToString(stageType.RO32), + }, + { + id: stageType.RO128, + label: stageTypeEnumToString(stageType.RO128), + }, + { + id: stageType.SEMI_FINAL, + label: stageTypeEnumToString(stageType.SEMI_FINAL), + }, + ]; + + const [stateCreateMatch, formCreateMatch] = useTypeSafeFormState( + addMatchSchema, + async (data) => { + const addMatchActionResponse = await addMatchAction(data); + if (!addMatchActionResponse.status) { + return toast.error(addMatchActionResponse.errorMessage, { + duration: 3000, + }); + } + modalRef.current?.close(); + // resetFormValues({ + // formRef, + // resetWithoutInputNames: ["tournamentAbbreviation"], + // schema: createQualificationRoomsSchema, + // }); // todo, czyscieczenie + }, + ); + + const [stateEditQRoom, formActionEditQRoom] = useTypeSafeFormState( + editQualificationRoomsSchema, + async (data) => { + const editMemberResponse = await editQualificationRoomsAction(data); + if (!editMemberResponse.status) { + return toast.error(editMemberResponse.errorMessage, { + duration: 3000, + }); + } + modalRef.current?.close(); + resetFormValues({ + formRef, + resetWithoutInputNames: ["tournamentAbbreviation", "roomId", "dataTimeStart"], + schema: editQualificationRoomsSchema, + }); + }, + ); // todo nie dziala to sa w ogole glupoty tutaj + + return ( + <> + {modalType.type === "add" ? ( + + ) : ( + + )} + + +

    {modalType.type === "add" ? "Create match" : `Edit qualification rooms`}

    +
    +
    + + + + stage.id === modalType.stage.stageType, + // )?.id || "", + // ] + // : [] + // } + readonly={modalType.type === "edit" ? true : false} + /> + stage.id === modalType.stage.stageType, + // )?.id || "", + // ] + // : [] + // } + // readonly={modalType.type === "edit" ? true : false} + /> + stage.id === modalType.stage.stageType, + // )?.id || "", + // ] + // : [] + // } + // readonly={modalType.type === "edit" ? true : false} + /> + staff.id) + : [] + } + /> + + staff.id) + : [] + } + /> + + staff.id) + : [] + } + /> +
    + +
    +
    + + ); +}; diff --git a/src/app/(main-page)/(withAuth)/dashboard/[tournamentAbbreviation]/matches/RescheduleMatchModal.tsx b/src/app/(main-page)/(withAuth)/dashboard/[tournamentAbbreviation]/matches/RescheduleMatchModal.tsx new file mode 100644 index 0000000..9c73c42 --- /dev/null +++ b/src/app/(main-page)/(withAuth)/dashboard/[tournamentAbbreviation]/matches/RescheduleMatchModal.tsx @@ -0,0 +1,136 @@ +"use client"; +import React, { useRef } from "react"; +import { toast } from "sonner"; +import { useFormStatus } from "react-dom"; +import { Button } from "@ui/atoms/Button/Button"; +import Modal from "@ui/organisms/Modal/Modal"; +import { Input } from "@ui/atoms/Forms/Input/Input"; +import { useTypeSafeFormState } from "@/hooks/useTypeSafeFormState"; +import { rescheduleMatchAction } from "@/actions/public/rescheduleMatchAction"; +import { rescheduleMatchSchema } from "@/formSchemas/rescheduleMatchSchema"; + +interface RescheduleMatchModalProps { + tournamentAbb: string; + matchId: string; + currentMatchDate: string; +} + +const SubmitButton = ({ onCancel }: { onCancel: () => void }) => { + const { pending } = useFormStatus(); + return ( + <> + + + + ); +}; + +export const RescheduleMatchModal = ({ + tournamentAbb, + matchId, + currentMatchDate, +}: RescheduleMatchModalProps) => { + const modalRef = useRef(null); + const formRef = React.useRef(null); + + const [state, formAction] = useTypeSafeFormState( + rescheduleMatchSchema, + async (data) => { + const response = await rescheduleMatchAction(data); + if (!response.status) { + return toast.error(response.errorMessage, { + duration: 3000, + }); + } + toast.success("Reschedule request sent successfully", { + duration: 3000, + }); + modalRef.current?.close(); + formRef.current?.reset(); + }, + ); + + return ( + <> + + + +

    Reschedule Match

    +
    +
    + + + + + {state?.errors.agreeToRules && ( +

    {state.errors.agreeToRules[0]}

    + )} +
    +
    + { + modalRef.current?.close(); + formRef.current?.reset(); + }} + /> +
    +
    +
    + + ); +}; + + diff --git a/src/app/(main-page)/(withAuth)/dashboard/[tournamentAbbreviation]/matches/page.tsx b/src/app/(main-page)/(withAuth)/dashboard/[tournamentAbbreviation]/matches/page.tsx index a60be8f..a08e8d4 100644 --- a/src/app/(main-page)/(withAuth)/dashboard/[tournamentAbbreviation]/matches/page.tsx +++ b/src/app/(main-page)/(withAuth)/dashboard/[tournamentAbbreviation]/matches/page.tsx @@ -1,7 +1,575 @@ import React from "react"; +import Image from "next/image"; +import { format } from "date-fns"; +import { cookies } from "next/headers"; +import { + client, + getMatches, + getParticipants, + getStaffMembers1, + getTeamsByTournament, + getTournamentByAbbreviation, + getTournamentStaffMember, + signInMatchStaffMember, + type StaffMemberResponseDto, + tournamentType, +} from "../../../../../../../client"; +import { type selectOptions } from "@ui/atoms/Forms/Select/ComboBox"; +import { getUser } from "@/actions/public/getUserAction"; +import { multipleRevalidatePaths } from "@/lib/multipleRevalidatePaths"; +import { MatchesModal } from "@/app/(main-page)/(withAuth)/dashboard/[tournamentAbbreviation]/matches/MatchesModal"; +import { EditMatchModal } from "@/app/(main-page)/(withAuth)/dashboard/[tournamentAbbreviation]/matches/EditMatchModal"; +import { deleteMatchAction } from "@/actions/admin/adminDeleteMatchActions"; -const MatchesPage = () => { - return
    nie ma pospiechu L)
    ; +const MatchesPage = async ({ + params: { tournamentAbbreviation }, +}: { + params: { + tournamentAbbreviation: string; + }; +}) => { + const cookie = cookies().get("JWT")?.value; + // configure internal service client + client.setConfig({ + // set default base url for requests + baseUrl: process.env.NEXT_PUBLIC_API_URL, + // set default headers for requests + headers: { + Cookie: `token=${cookie}`, + }, + }); + const userData = await getUser(); + const { data: getStaffForATournamentUser } = await getTournamentStaffMember({ + path: { + abbreviation: tournamentAbbreviation, + }, + }); + + const { data: getStaffMembers } = await getStaffMembers1({ + path: { + abbreviation: tournamentAbbreviation, + }, + }); + + const { data: getTournament } = await getTournamentByAbbreviation({ + path: { + abbreviation: tournamentAbbreviation, + }, + }); + + const staffMemberSelectOptions: selectOptions[] = + getStaffMembers + ?.filter((s) => s.user) + ?.map((staffMember) => ({ + id: staffMember.id, + label: staffMember.user ? staffMember.user.username : staffMember.username || "", + })) || []; + + let rostersSelectOptions: selectOptions[] = []; + if (getTournament?.tournamentType !== tournamentType.PARTICIPANT_VS) { + const { data: getTeams } = await getTeamsByTournament({ + path: { + abbreviation: tournamentAbbreviation, + }, + }); + rostersSelectOptions = + getTeams?.map((team) => ({ + id: team.id, + label: team.name, + })) || []; + } else { + const { data: getParticipantsData } = await getParticipants({ + path: { + abbreviation: tournamentAbbreviation, + }, + }); + rostersSelectOptions = + getParticipantsData?.map((participant) => ({ + id: participant.id, + label: participant.user.username, + })) || []; + } + + const { data } = await getMatches({ + path: { + abbreviation: tournamentAbbreviation, + }, + }); + + const canSignIn = + getStaffForATournamentUser?.permissions?.some((permission) => { + const b = permission === "MATCH_STAFF_MEMBER_SIGN_IN"; + return b; + }) || + getStaffForATournamentUser?.roles?.some((role) => + role.permissions.some((permission) => permission === "MATCH_STAFF_MEMBER_SIGN_IN"), + ); + + const isHost = + getStaffForATournamentUser?.roles?.some((role) => role.name === "Host") || false; + + const showSignOut = (staffMembers: StaffMemberResponseDto[] | undefined) => { + return ( + staffMembers?.some((staffMember) => { + return staffMember.user?.id === userData?.id; + }) && canSignIn + ); + }; + + const showSignIn = (staffMembers: StaffMemberResponseDto[] | undefined) => { + return ( + canSignIn && + !staffMembers?.some((staffMember) => { + return staffMember.user?.id === userData?.id; + }) + ); + }; + + + return ( +
    +

    Matches

    + + +
    +
    + + + + + + + + + + + + {isHost && } + + + + {data + ?.filter((match) => match.matchResult === null) + ?.sort((a, b) => { + return a.startDate > b.startDate ? 1 : -1; + }) + .map((match) => ( + + + + + + + + + + {isHost && ( + + )} + + ))} + +
    Match IDStart date time (UTC+0)StageTeam blueTeam redRefereeCommentatorsStreamersActions
    {match.matchId} + {format(new Date(match.startDate), "dd/MM/yyyy HH:mm")} + {match.stage?.stageType}{match.teamBlue.name}{match.teamRed.name} +
    + {match.referees?.map((referee) => ( +
    +
    +
    + referee +
    +
    + {referee?.user?.username} +
    + ))} +
    + {showSignOut(match.referees) && ( +
    { + "use server"; + const cookie = cookies().get("JWT")?.value; + // configure internal service client + client.setConfig({ + // set default base url for requests + baseUrl: + process.env.NEXT_PUBLIC_API_URL, + // set default headers for requests + headers: { + Cookie: `token=${cookie}`, + }, + }); + await signInMatchStaffMember({ + path: { + abbreviation: + tournamentAbbreviation, + matchId: match.id, + }, + query: { + in: false, + type: "REFEREE", + }, + }); + await multipleRevalidatePaths([ + "/", + `/dashboard/${tournamentAbbreviation}/qualification-rooms`, + `/tournament/${tournamentAbbreviation}/schedule`, + ]); + }} + > + +
    + )} + {showSignIn(match.referees) && ( +
    { + "use server"; + const cookie = cookies().get("JWT")?.value; + // configure internal service client + client.setConfig({ + // set default base url for requests + baseUrl: + process.env.NEXT_PUBLIC_API_URL, + // set default headers for requests + headers: { + Cookie: `token=${cookie}`, + }, + }); + await signInMatchStaffMember({ + path: { + abbreviation: + tournamentAbbreviation, + matchId: match.id, + }, + query: { + in: true, + type: "REFEREE", + }, + }); + await multipleRevalidatePaths([ + "/", + `/dashboard/${tournamentAbbreviation}/qualification-rooms`, + `/tournament/${tournamentAbbreviation}/schedule`, + ]); + }} + > + +
    + )} +
    +
    + {match.commentators?.map((com) => ( +
    +
    +
    + referee +
    +
    + {com?.user?.username} +
    + ))} +
    + {showSignOut(match.commentators) && ( +
    { + "use server"; + const cookie = cookies().get("JWT")?.value; + // configure internal service client + client.setConfig({ + // set default base url for requests + baseUrl: + process.env.NEXT_PUBLIC_API_URL, + // set default headers for requests + headers: { + Cookie: `token=${cookie}`, + }, + }); + await signInMatchStaffMember({ + path: { + abbreviation: + tournamentAbbreviation, + matchId: match.id, + }, + query: { + in: false, + type: "COMMENTATOR", + }, + }); + await multipleRevalidatePaths([ + "/", + `/dashboard/${tournamentAbbreviation}/qualification-rooms`, + `/tournament/${tournamentAbbreviation}/schedule`, + ]); + }} + > + +
    + )} + {showSignIn(match.commentators) && ( +
    { + "use server"; + const cookie = cookies().get("JWT")?.value; + // configure internal service client + client.setConfig({ + // set default base url for requests + baseUrl: + process.env.NEXT_PUBLIC_API_URL, + // set default headers for requests + headers: { + Cookie: `token=${cookie}`, + }, + }); + await signInMatchStaffMember({ + path: { + abbreviation: + tournamentAbbreviation, + matchId: match.id, + }, + query: { + in: true, + type: "COMMENTATOR", + }, + }); + await multipleRevalidatePaths([ + "/", + `/dashboard/${tournamentAbbreviation}/qualification-rooms`, + `/tournament/${tournamentAbbreviation}/schedule`, + ]); + }} + > + +
    + )} +
    +
    + {match.streamers?.map((str) => ( +
    +
    +
    + referee +
    +
    + {str?.user?.username} +
    + ))} +
    + {showSignOut(match.streamers) && ( +
    { + "use server"; + const cookie = cookies().get("JWT")?.value; + // configure internal service client + client.setConfig({ + // set default base url for requests + baseUrl: + process.env.NEXT_PUBLIC_API_URL, + // set default headers for requests + headers: { + Cookie: `token=${cookie}`, + }, + }); + await signInMatchStaffMember({ + path: { + abbreviation: + tournamentAbbreviation, + matchId: match.id, + }, + query: { + in: false, + type: "STREAMER", + }, + }); + await multipleRevalidatePaths([ + "/", + `/dashboard/${tournamentAbbreviation}/qualification-rooms`, + `/tournament/${tournamentAbbreviation}/schedule`, + ]); + }} + > + +
    + )} + {showSignIn(match.streamers) && ( +
    { + "use server"; + const cookie = cookies().get("JWT")?.value; + // configure internal service client + client.setConfig({ + // set default base url for requests + baseUrl: + process.env.NEXT_PUBLIC_API_URL, + // set default headers for requests + headers: { + Cookie: `token=${cookie}`, + }, + }); + await signInMatchStaffMember({ + path: { + abbreviation: + tournamentAbbreviation, + matchId: match.id, + }, + query: { + in: true, + type: "STREAMER", + }, + }); + await multipleRevalidatePaths([ + "/", + `/dashboard/${tournamentAbbreviation}/qualification-rooms`, + `/tournament/${tournamentAbbreviation}/schedule`, + ]); + }} + > + +
    + )} +
    +
    + +
    { + "use server"; + const result = await deleteMatchAction( + tournamentAbbreviation, + match.id, + ); + if (!result.status) { + throw new Error(result.errorMessage); + } + }} + > + +
    +
    +
    +
    +
    + +
    +

    Finished matches

    + +
    + + + + + + + + + + + + + + {data + ?.filter((match) => match.matchResult !== null) + ?.sort((a, b) => { + return a.startDate > b.startDate ? 1 : -1; + }) + .map((match) => ( + + + + + + + + + + ))} + +
    Start date time (UTC+0)StageTeam blueTeam redRefereeCommentatorsStreamers
    + {format(new Date(match.startDate), "dd/MM/yyyy HH:mm")} + {match.stage?.stageType}{match.teamBlue.name}{match.teamRed.name} + {match.referees?.map((referee) => ( +
    + {referee?.user?.username} +
    + ))} +
    + {match.commentators?.map((commentator) => ( +
    + {commentator.user?.username} +
    + ))} +
    + {match.streamers?.map((streamer) => ( +
    + {streamer?.user?.username} +
    + ))} +
    +
    +
    +
    + ); }; export default MatchesPage; diff --git a/src/app/(main-page)/(withAuth)/dashboard/[tournamentAbbreviation]/qualification-rooms/QualificationRoomModal.tsx b/src/app/(main-page)/(withAuth)/dashboard/[tournamentAbbreviation]/qualification-rooms/QualificationRoomModal.tsx index 8f000f0..ad15cac 100644 --- a/src/app/(main-page)/(withAuth)/dashboard/[tournamentAbbreviation]/qualification-rooms/QualificationRoomModal.tsx +++ b/src/app/(main-page)/(withAuth)/dashboard/[tournamentAbbreviation]/qualification-rooms/QualificationRoomModal.tsx @@ -154,7 +154,7 @@ export const QualificationRoomModal = ({ tournamentAbb, modalType }: IAddStaffMe name={"offset"} label={"Offset"} required={true} - max={24} + min={1} disabled={modalType.type === "edit"} errorMessage={ stateAddNewQRoom?.errors.offset && diff --git a/src/app/(main-page)/(withAuth)/dashboard/[tournamentAbbreviation]/qualification-rooms/page.tsx b/src/app/(main-page)/(withAuth)/dashboard/[tournamentAbbreviation]/qualification-rooms/page.tsx index cf942aa..7d56de5 100644 --- a/src/app/(main-page)/(withAuth)/dashboard/[tournamentAbbreviation]/qualification-rooms/page.tsx +++ b/src/app/(main-page)/(withAuth)/dashboard/[tournamentAbbreviation]/qualification-rooms/page.tsx @@ -1,53 +1,43 @@ import React from "react"; import Image from "next/image"; import { format } from "date-fns"; +import { cookies } from "next/headers"; import { - AdminQualificationService, - AdminStaffMemberService, - type ParticipantResponseDto, - ParticipantService, + client, + deleteQualificationRoom, + getParticipants, + getQualificationRooms, + getStaffMembers1, + getTeamsByTournament, + getTournamentByAbbreviation, + getTournamentStaffMember, type QualificationRoomResponseDto, - QualificationService, - type StaffMemberResponseDto, - type TeamResponseDto, - TeamService, - TournamentRequestDto, - TournamentService, - UserService, -} from "../../../../../../../generated"; + signInOutQualificationRoom, + tournamentType, +} from "../../../../../../../client"; import { QualificationRoomModal } from "@/app/(main-page)/(withAuth)/dashboard/[tournamentAbbreviation]/qualification-rooms/QualificationRoomModal"; -import { executeFetch } from "@/lib/executeFetch"; import { type selectOptions } from "@ui/atoms/Forms/Select/ComboBox"; import { getUser } from "@/actions/public/getUserAction"; - -interface QualificationRoom extends QualificationRoomResponseDto { - id: string; - number: number; - isClosed: number; - maxSlots: number; - occupiedSlots: number; - staffMember: StaffMemberResponseDto; - startDate: string; -} - -interface TeamBasedQualificationRoom extends QualificationRoom { - teams: TeamResponseDto[]; -} - -interface ParticipantBasedQualificationRoom extends QualificationRoom { - participants: ParticipantResponseDto[]; -} +import { multipleRevalidatePaths } from "@/lib/multipleRevalidatePaths"; +import { + type ParticipantBasedQualificationRoom, + type TeamBasedQualificationRoom, +} from "@/models/QualificationRoom"; const qualificationRooms = ( rooms: QualificationRoomResponseDto[], ): (TeamBasedQualificationRoom | ParticipantBasedQualificationRoom)[] => { - return rooms.map((room) => { - if (room.tournamentType !== TournamentRequestDto.tournamentType.PARTICIPANT_VS) { - return room as TeamBasedQualificationRoom; - } else { - return room as ParticipantBasedQualificationRoom; - } - }); + return rooms + .map((room) => { + if (room.tournamentType !== tournamentType.PARTICIPANT_VS) { + return room as TeamBasedQualificationRoom; + } else { + return room as ParticipantBasedQualificationRoom; + } + }) + .sort((a, b) => { + return a.startDate > b.startDate ? 1 : -1; + }); }; const QRoomsPage = async ({ @@ -57,87 +47,84 @@ const QRoomsPage = async ({ tournamentAbbreviation: string; }; }) => { + const cookie = cookies().get("JWT")?.value; + // configure internal service client + client.setConfig({ + // set default base url for requests + baseUrl: process.env.NEXT_PUBLIC_API_URL, + // set default headers for requests + headers: { + Cookie: `token=${cookie}`, + }, + }); const userData = await getUser(); - const getStaffForATournamentUser = await executeFetch( - UserService.getTournamentStaffMember(tournamentAbbreviation), - ); - - const getQualificationRooms = await executeFetch( - QualificationService.getQualificationRooms(tournamentAbbreviation), - ); - const getStaffMembers = await executeFetch( - AdminStaffMemberService.getStaffMembers1(tournamentAbbreviation), - ); - - const getTournament = await executeFetch( - TournamentService.getTournamentByAbbreviation(tournamentAbbreviation), - ); - - if (!getStaffForATournamentUser.status) { - return
    {getStaffForATournamentUser.errorMessage}
    ; - } + const { data: getStaffForATournamentUser } = await getTournamentStaffMember({ + path: { + abbreviation: tournamentAbbreviation, + }, + }); - if (!getStaffMembers.status) { - return
    {getStaffMembers.errorMessage}
    ; - } + const { data: getQualificationRoomsData } = await getQualificationRooms({ + path: { abbreviation: tournamentAbbreviation }, + }); - if (!getQualificationRooms.status) { - return
    {getQualificationRooms.errorMessage}
    ; - } + const { data: getStaffMembers } = await getStaffMembers1({ + path: { + abbreviation: tournamentAbbreviation, + }, + }); - if (!getTournament.status) { - return
    {getTournament.errorMessage}
    ; - } + const { data: getTournament } = await getTournamentByAbbreviation({ + path: { + abbreviation: tournamentAbbreviation, + }, + }); - const staffMemberSelectOptions: selectOptions[] = getStaffMembers.response - .filter((s) => s.user) - .map((staffMember) => ({ - id: staffMember.id, - label: staffMember.user ? staffMember.user.username : staffMember.username || "", - })); + const staffMemberSelectOptions: selectOptions[] = + getStaffMembers + ?.filter((s) => s.user) + ?.map((staffMember) => ({ + id: staffMember.id, + label: staffMember.user ? staffMember.user.username : staffMember.username || "", + })) || []; let rostersSelectOptions: selectOptions[] = []; - if ( - getTournament.response.tournamentType !== TournamentRequestDto.tournamentType.PARTICIPANT_VS - ) { - const getTeams = await executeFetch( - TeamService.getTeamsByTournament(tournamentAbbreviation), - ); - if (!getTeams.status) { - return
    {getTeams.errorMessage}
    ; - } - rostersSelectOptions = getTeams.response.map((team) => ({ - id: team.id, - label: team.name, - })); + if (getTournament?.tournamentType !== tournamentType.PARTICIPANT_VS) { + const { data: getTeams } = await getTeamsByTournament({ + path: { + abbreviation: tournamentAbbreviation, + }, + }); + rostersSelectOptions = + getTeams?.map((team) => ({ + id: team.id, + label: team.name, + })) || []; } else { - const getParticipants = await executeFetch( - ParticipantService.getParticipants(tournamentAbbreviation), - ); - if (!getParticipants.status) { - return
    {getParticipants.errorMessage}
    ; - } - rostersSelectOptions = getParticipants.response.map((participant) => ({ - id: participant.id, - label: participant.user.username, - })); + const { data: getParticipantsData } = await getParticipants({ + path: { + abbreviation: tournamentAbbreviation, + }, + }); + rostersSelectOptions = + getParticipantsData?.map((participant) => ({ + id: participant.id, + label: participant.user.username, + })) || []; } const canSignIn = - getStaffForATournamentUser.response.roles?.some((role) => + getStaffForATournamentUser?.roles?.some((role) => role.permissions.some( (permission) => permission === "QUALIFICATION_ROOM_STAFF_MEMBER_SIGN_IN", ), ) || - getStaffForATournamentUser.response.permissions?.some( + getStaffForATournamentUser?.permissions?.some( (permission) => permission === "QUALIFICATION_ROOM_STAFF_MEMBER_SIGN_IN", ); return (
    - {` - brakujue sign out :) - `}

    Qualification rooms

    NumberStart date timeStart date time (UTC+0) Roster Referee Actions
    {room.number}{format(new Date(room.startDate), "dd/MM/yyyy hh:mm")} - {room.tournamentType === - TournamentRequestDto.tournamentType.PARTICIPANT_VS + {format(new Date(room.startDate), "dd/MM/yyyy HH:mm")} + {room.tournamentType === tournamentType.PARTICIPANT_VS ? ( room as ParticipantBasedQualificationRoom ).participants.map((participant) => ( @@ -173,21 +159,58 @@ const QRoomsPage = async ({ )) : (room as TeamBasedQualificationRoom).teams.map((team) => ( -
    - {team.name} +
    {team.name} + {team.name}
    ))}
    {room.staffMember ? ( userData?.id === room.staffMember.user?.id ? ( -
    Sign out:todo
    +
    { + "use server"; + const cookie = cookies().get("JWT")?.value; + // configure internal service client + client.setConfig({ + // set default base url for requests + baseUrl: process.env.NEXT_PUBLIC_API_URL, + // set default headers for requests + headers: { + Cookie: `token=${cookie}`, + }, + }); + await signInOutQualificationRoom({ + path: { + abbreviation: tournamentAbbreviation, + roomId: room.id, + }, + query: { + in: false, + }, + }); + await multipleRevalidatePaths([ + "/", + `/dashboard/${tournamentAbbreviation}/qualification-rooms`, + ]); + }} + > + +
    ) : (
    @@ -207,16 +230,29 @@ const QRoomsPage = async ({
    { "use server"; - await executeFetch( - AdminQualificationService.signInQualificationRoom( - tournamentAbbreviation, - room.id, - ), - [ - "/", - "/dashboard/[tournamentAbb]/qualification-rooms", - ], - ); + const cookie = cookies().get("JWT")?.value; + // configure internal service client + client.setConfig({ + // set default base url for requests + baseUrl: process.env.NEXT_PUBLIC_API_URL, + // set default headers for requests + headers: { + Cookie: `token=${cookie}`, + }, + }); + await signInOutQualificationRoom({ + path: { + abbreviation: tournamentAbbreviation, + roomId: room.id, + }, + query: { + in: true, + }, + }); + await multipleRevalidatePaths([ + "/", + `/dashboard/${tournamentAbbreviation}/qualification-rooms`, + ]); }} >
    diff --git a/src/app/(main-page)/(withAuth)/dashboard/[tournamentAbbreviation]/staff-members/StaffMemberModal.tsx b/src/app/(main-page)/(withAuth)/dashboard/[tournamentAbbreviation]/staff-members/StaffMemberModal.tsx index a300e5f..073ea29 100644 --- a/src/app/(main-page)/(withAuth)/dashboard/[tournamentAbbreviation]/staff-members/StaffMemberModal.tsx +++ b/src/app/(main-page)/(withAuth)/dashboard/[tournamentAbbreviation]/staff-members/StaffMemberModal.tsx @@ -57,7 +57,7 @@ export const StaffMemberModal = ({ async (data) => { const addStaffMemberResponse = await addStaffMemberAction(data); if (!addStaffMemberResponse.status) { - return toast.error(addStaffMemberResponse.errorMessage, { + return toast.error("Error", { duration: 3000, }); } @@ -75,7 +75,7 @@ export const StaffMemberModal = ({ async (data) => { const editMemberResponse = await editStaffMemberAction(data); if (!editMemberResponse.status) { - return toast.error(editMemberResponse.errorMessage, { + return toast.error("error", { duration: 3000, }); } diff --git a/src/app/(main-page)/(withAuth)/dashboard/[tournamentAbbreviation]/staff-members/UserLessStaffMemberModal.tsx b/src/app/(main-page)/(withAuth)/dashboard/[tournamentAbbreviation]/staff-members/UserLessStaffMemberModal.tsx index 6b13f68..c96c1e3 100644 --- a/src/app/(main-page)/(withAuth)/dashboard/[tournamentAbbreviation]/staff-members/UserLessStaffMemberModal.tsx +++ b/src/app/(main-page)/(withAuth)/dashboard/[tournamentAbbreviation]/staff-members/UserLessStaffMemberModal.tsx @@ -46,7 +46,7 @@ export const UserLessStaffMemberModal = ({ async (data) => { const addStaffMemberResponse = await addUserLessStaffMemberAction(data); if (!addStaffMemberResponse.status) { - return toast.error(addStaffMemberResponse.errorMessage, { + return toast.error("error", { duration: 3000, }); } diff --git a/src/app/(main-page)/(withAuth)/dashboard/[tournamentAbbreviation]/staff-members/page.tsx b/src/app/(main-page)/(withAuth)/dashboard/[tournamentAbbreviation]/staff-members/page.tsx index a0b33ca..20bb5f7 100644 --- a/src/app/(main-page)/(withAuth)/dashboard/[tournamentAbbreviation]/staff-members/page.tsx +++ b/src/app/(main-page)/(withAuth)/dashboard/[tournamentAbbreviation]/staff-members/page.tsx @@ -1,13 +1,17 @@ import React from "react"; import Image from "next/image"; +import { cookies } from "next/headers"; import { - AdminStaffMemberService, - AdminTournamentRolesService, -} from "../../../../../../../generated"; + client, + deleteStaffMembers, + getStaffMembers1, + getTournamentPermissions, + getTournamentRoles, +} from "../../../../../../../client"; import { StaffMemberModal } from "@/app/(main-page)/(withAuth)/dashboard/[tournamentAbbreviation]/staff-members/StaffMemberModal"; -import { executeFetch } from "@/lib/executeFetch"; import type { selectOptions } from "@ui/atoms/Forms/Select/ComboBox"; import { UserLessStaffMemberModal } from "@/app/(main-page)/(withAuth)/dashboard/[tournamentAbbreviation]/staff-members/UserLessStaffMemberModal"; +import { multipleRevalidatePaths } from "@/lib/multipleRevalidatePaths"; const StaffMembersPage = async ({ params: { tournamentAbbreviation }, @@ -16,32 +20,41 @@ const StaffMembersPage = async ({ tournamentAbbreviation: string; }; }) => { - const getRoles = await executeFetch( - AdminTournamentRolesService.getTournamentRoles(tournamentAbbreviation), - ); - const getPermissions = await executeFetch( - AdminTournamentRolesService.getTournamentPermissions(tournamentAbbreviation), - ); - - const getStaffMembers = await executeFetch( - AdminStaffMemberService.getStaffMembers1(tournamentAbbreviation), - ); + const cookie = cookies().get("JWT")?.value; + // configure internal service client + client.setConfig({ + // set default base url for requests + baseUrl: process.env.NEXT_PUBLIC_API_URL, + // set default headers for requests + headers: { + Cookie: `token=${cookie}`, + }, + }); + const { data: getRoles } = await getTournamentRoles({ + path: { + abbreviation: tournamentAbbreviation, + }, + }); + const { data: getPermissions } = await getTournamentPermissions({ + path: { + abbreviation: tournamentAbbreviation, + }, + }); - if (!getRoles.status || !getPermissions.status) { - return
    Failed to fetch roles or permissions
    ; - } + const { data: getStaffMembers } = await getStaffMembers1({ + path: { + abbreviation: tournamentAbbreviation, + }, + }); - if (!getStaffMembers.status) { - return
    Failed to fetch staff members
    ; - } - - const rolesSelectOptions: selectOptions[] = getRoles.response?.map((role) => ({ - id: role.id, - label: role.name, - })); + const rolesSelectOptions: selectOptions[] = + getRoles?.map((role) => ({ + id: role.id, + label: role.name, + })) || []; const permissionsSelectOptions: selectOptions[] = - getPermissions.response.permissions?.map((permission) => ({ + getPermissions?.permissions?.map((permission) => ({ id: permission, label: permission, })) || []; @@ -79,7 +92,7 @@ const StaffMembersPage = async ({
    {s.user ? s.user.osuId : "-"} @@ -154,16 +167,26 @@ const StaffMembersPage = async ({ { "use server"; - await executeFetch( - AdminStaffMemberService.deleteStaffMembers( - tournamentAbbreviation, - s.id, - ), - [ - "/", - `/dashboard/${tournamentAbbreviation}/staff-members`, - ], - ); + const cookie = cookies().get("JWT")?.value; + // configure internal service client + client.setConfig({ + // set default base url for requests + baseUrl: process.env.NEXT_PUBLIC_API_URL, + // set default headers for requests + headers: { + Cookie: `token=${cookie}`, + }, + }); + await deleteStaffMembers({ + path: { + abbreviation: tournamentAbbreviation, + staffMemberId: s.id, + }, + }); + await multipleRevalidatePaths([ + "/", + `/dashboard/${tournamentAbbreviation}/staff-members`, + ]); }} >
    + + + + + + + + + + + {participants.map((participant) => ( + + + + + + + + ))} + +
    AvatarUsernameosu! IDStatusActions
    +
    +
    + {participant.user.username} +
    +
    +
    +
    {participant.user.username}
    +
    +
    {participant.user.osuId}
    +
    + {participant.id === captain.id ? ( + Captain + ) : ( + Member + )} + +
    + {participant.id !== captain.id ? ( + <> + + + + ) : ( + + Captain cannot be removed + + )} +
    +
    +
    + +
    + + +
    + + + + {/* Render remove participant modals outside the main modal to avoid nested forms */} + {participants.map((participant) => ( + participant.id !== captain.id && removeModalRefs[participant.id] && ( + handleRemoveParticipant(participant)} + isRemoving={isRemovingParticipant === participant.id} + /> + ) + ))} + + ); +}; + diff --git a/src/app/(main-page)/(withAuth)/dashboard/[tournamentAbbreviation]/teams/RemoveParticipantModal.tsx b/src/app/(main-page)/(withAuth)/dashboard/[tournamentAbbreviation]/teams/RemoveParticipantModal.tsx new file mode 100644 index 0000000..2d68808 --- /dev/null +++ b/src/app/(main-page)/(withAuth)/dashboard/[tournamentAbbreviation]/teams/RemoveParticipantModal.tsx @@ -0,0 +1,73 @@ +"use client"; + +import React from "react"; +import type { ParticipantResponseDto } from "../../../../../../../client"; +import { Button } from "@ui/atoms/Button/Button"; +import Modal from "@ui/organisms/Modal/Modal"; + +interface IRemoveParticipantModalProps { + modalRef: React.RefObject; + participant: ParticipantResponseDto; + onConfirm: () => Promise; + isRemoving?: boolean; +} + +export const RemoveParticipantModal = ({ + modalRef, + participant, + onConfirm, + isRemoving = false, +}: IRemoveParticipantModalProps) => { + + const handleConfirmRemove = async () => { + try { + await onConfirm(); + modalRef.current?.close(); + } catch (error) { + // Error handling is done in parent component + } + }; + + const handleCancel = () => { + modalRef.current?.close(); + }; + + return ( + +

    Remove Participant

    +
    +

    + Are you sure you want to remove "{participant.user.username}" from + the team? +

    +
    +

    Warning:

    +

    + This action will permanently remove the participant from the team and delete + their participation data. +

    +

    This action cannot be undone.

    +
    +
    + + +
    +
    +
    + ); +}; + diff --git a/src/app/(main-page)/(withAuth)/dashboard/[tournamentAbbreviation]/teams/TeamsTable.tsx b/src/app/(main-page)/(withAuth)/dashboard/[tournamentAbbreviation]/teams/TeamsTable.tsx new file mode 100644 index 0000000..31f97fd --- /dev/null +++ b/src/app/(main-page)/(withAuth)/dashboard/[tournamentAbbreviation]/teams/TeamsTable.tsx @@ -0,0 +1,192 @@ +"use client"; + +import React, { useState, useTransition } from "react"; +import Image from "next/image"; +import { useRouter } from "next/navigation"; +import { toast } from "sonner"; +import type { TeamResponseDto, ParticipantResponseDto } from "../../../../../../../client"; +import { EditTeamModal } from "./EditTeamModal"; +import { DeleteTeamModal } from "./DeleteTeamModal"; +import { AvatarGroup } from "@ui/atoms/AvatarGroup/AvatarGroup"; +import { changeTeamStatusAction } from "@/actions/admin/adminTeamActions"; + +interface TeamsTableProps { + teams: TeamResponseDto[]; + tournamentAbbreviation: string; + canUpdateTeam: boolean; + canDeleteTeam: boolean; + canChangeTeamStatus: boolean; +} + +export const TeamsTable = ({ + teams, + tournamentAbbreviation, + canUpdateTeam, + canDeleteTeam, + canChangeTeamStatus, +}: TeamsTableProps) => { + const [isCompact, setIsCompact] = useState(false); + const [isPending, startTransition] = useTransition(); + const router = useRouter(); + + // Filter out captain from participants for compact view + const getNonCaptainParticipants = (team: TeamResponseDto): ParticipantResponseDto[] => { + return team.participants.filter( + (participant) => participant.id !== team.captain.id, + ); + }; + + const handleChangeTeamStatus = async (teamId: string, status: "ACCEPTED" | "REJECTED") => { + startTransition(async () => { + const result = await changeTeamStatusAction(tournamentAbbreviation, teamId, status); + if (result.status) { + toast.success(`Team status changed to ${status}`, { + duration: 3000, + }); + router.refresh(); + } else { + const errorMsg: string = result.errorMessage || "Failed to change team status"; + toast.error(errorMsg, { + duration: 3000, + }); + } + }); + }; + + return ( +
    +
    +

    Teams

    + +
    +
    + + + + + + + + + + + + {teams.map((team) => { + const nonCaptainParticipants = getNonCaptainParticipants(team); + return ( + + + + + + + + ); + })} + +
    Team nameCaptainRosterStatusActions
    +
    +
    +
    + Team logo +
    +
    + {team.name} +
    +
    +
    +
    +
    + {team.captain.user.username} +
    +
    + {team.captain.user.username} +
    +
    + {isCompact && nonCaptainParticipants.length > 0 ? ( + + ) : ( +
    + {team.participants.map((participant) => ( +
    +
    +
    + {participant.user.username} +
    +
    + {participant.user.username} +
    + ))} +
    + )} +
    {team.status} +
    + {canChangeTeamStatus && ( + <> + + + + )} + {canUpdateTeam && ( + + )} + {canDeleteTeam && ( + + )} +
    +
    +
    +
    + ); +}; + diff --git a/src/app/(main-page)/(withAuth)/dashboard/[tournamentAbbreviation]/teams/page.tsx b/src/app/(main-page)/(withAuth)/dashboard/[tournamentAbbreviation]/teams/page.tsx new file mode 100644 index 0000000..30eb420 --- /dev/null +++ b/src/app/(main-page)/(withAuth)/dashboard/[tournamentAbbreviation]/teams/page.tsx @@ -0,0 +1,78 @@ +import React from "react"; +import { cookies } from "next/headers"; +import { client, getTeams, getTournamentStaffMember } from "../../../../../../../client"; +import { TeamsTable } from "./TeamsTable"; + +const TeamsPage = async ({ + params: { tournamentAbbreviation }, +}: { + params: { + tournamentAbbreviation: string; + }; +}) => { + const cookie = cookies().get("JWT")?.value; + // configure internal service client + client.setConfig({ + // set default base url for requests + baseUrl: process.env.NEXT_PUBLIC_API_URL, + // set default headers for requests + headers: { + Cookie: `token=${cookie}`, + }, + }); + const { data, error } = await getTeams({ + path: { + abbreviation: tournamentAbbreviation, + }, + }); + if (error) { + return
    Failed to fetch teams
    ; + } + + const { data: getStaffForATournamentUser } = await getTournamentStaffMember({ + path: { + abbreviation: tournamentAbbreviation, + }, + }); + + // Check permissions + const canUpdateTeam = + getStaffForATournamentUser?.permissions?.some((permission) => { + return permission === "TEAM_UPDATE"; + }) || + getStaffForATournamentUser?.roles?.some((role) => + role.permissions.some((permission) => permission === "TEAM_UPDATE"), + ); + + const canDeleteTeam = + getStaffForATournamentUser?.permissions?.some((permission) => { + return permission === "TEAM_DELETE"; + }) || + getStaffForATournamentUser?.roles?.some((role) => + role.permissions.some((permission) => permission === "TEAM_DELETE"), + ); + + const canChangeTeamStatus = + getStaffForATournamentUser?.permissions?.some((permission) => { + return permission === "TEAM_STATUS_UPDATE"; + }) || + getStaffForATournamentUser?.roles?.some((role) => + role.permissions.some((permission) => permission === "TEAM_STATUS_UPDATE"), + ); + + if (!data) { + return
    Failed to fetch teams
    ; + } + + return ( + + ); +}; + +export default TeamsPage; diff --git a/src/app/(main-page)/(withAuth)/dashboard/page.tsx b/src/app/(main-page)/(withAuth)/dashboard/page.tsx index 6a498ce..4f26dab 100644 --- a/src/app/(main-page)/(withAuth)/dashboard/page.tsx +++ b/src/app/(main-page)/(withAuth)/dashboard/page.tsx @@ -1,22 +1,28 @@ import React from "react"; import Link from "next/link"; -import { TournamentService } from "../../../../../generated"; +import { cookies } from "next/headers"; +import { client, getTournaments } from "../../../../../client"; import CreateTournamentModal from "@/app/(main-page)/(withAuth)/dashboard/CreateTournamentModal"; -import { executeFetch } from "@/lib/executeFetch"; const DashboardPage = async () => { - const tournaments = await executeFetch(TournamentService.getTournaments()); - - if (!tournaments.status) { - return
    {tournaments.errorMessage}
    ; - } + const cookie = cookies().get("JWT")?.value; + // configure internal service client + client.setConfig({ + // set default base url for requests + baseUrl: process.env.NEXT_PUBLIC_API_URL, + // set default headers for requests + headers: { + Cookie: `token=${cookie}`, + }, + }); + const { data: tournaments } = await getTournaments(); return (

    Select tournament

    - {tournaments.response.map((tournament) => ( + {tournaments?.map((tournament) => ( diff --git a/src/app/(main-page)/page.tsx b/src/app/(main-page)/page.tsx index a82ee09..ed7054a 100644 --- a/src/app/(main-page)/page.tsx +++ b/src/app/(main-page)/page.tsx @@ -2,11 +2,23 @@ import React, { Suspense } from "react"; import Image from "next/image"; import { MdKeyboardArrowDown } from "react-icons/md"; import Link from "next/link"; +import { cookies } from "next/headers"; +import { client } from "../../../client"; import { TournamentList } from "@ui/organisms/TournamentList/TournamentList"; import { Loading } from "@ui/atoms/Loading/Loading"; import Section from "@ui/atoms/Section/Section"; const Main = async () => { + const cookie = cookies().get("JWT")?.value; + // configure internal service client + client.setConfig({ + // set default base url for requests + baseUrl: process.env.NEXT_PUBLIC_API_URL, + // set default headers for requests + headers: { + Cookie: `token=${cookie}`, + }, + }); return ( <>
    diff --git a/src/app/(tournament)/tournament/[tournamentId]/layout.tsx b/src/app/(tournament)/tournament/[tournamentId]/layout.tsx index 06b6657..ce96c97 100644 --- a/src/app/(tournament)/tournament/[tournamentId]/layout.tsx +++ b/src/app/(tournament)/tournament/[tournamentId]/layout.tsx @@ -1,14 +1,18 @@ import React from "react"; import Image from "next/image"; import NextTopLoader from "nextjs-toploader"; -import { StageService, TournamentRequestDto, TournamentService } from "../../../../../generated"; +import { cookies } from "next/headers"; +import { + client, + getQualificationRooms, + getStages, + getTournamentByAbbreviation, + tournamentType, +} from "../../../../../client"; import { type INavbarProps, Navbar } from "@ui/organisms/Navbar/Navbar"; import { Footer } from "@ui/organisms/Footer/Footer"; import { stageTypeEnumToString } from "@/lib/helpers"; -import tournamentType = TournamentRequestDto.tournamentType; import { LoginAvatar } from "@ui/molecules/LoginAvatar/LoginAvatar"; -import { executeFetch } from "@/lib/executeFetch"; -import Section from "@ui/atoms/Section/Section"; type ITournamentLayout = { children: React.ReactNode; @@ -19,6 +23,7 @@ type ITournamentLayout = { const navbarRoutes: INavbarProps[] = [ { name: "Home", href: "/" }, { name: "Rules", href: "/rules" }, + { name: "Qualification rooms", href: "/qualification-rooms" }, { name: "Schedule", href: "/schedule" }, { name: "Mappool", @@ -30,24 +35,52 @@ const navbarRoutes: INavbarProps[] = [ ]; export default async function Layout({ children, params }: ITournamentLayout) { - const getStagesData = await executeFetch(StageService.getStages(params.tournamentId)); + const cookie = cookies().get("JWT")?.value; + // configure internal service client + client.setConfig({ + // set default base url for requests + baseUrl: process.env.NEXT_PUBLIC_API_URL, + // set default headers for requests + headers: { + Cookie: `token=${cookie}`, + }, + }); + const { data: getStagesData } = await getStages({ + path: { + abbreviation: params.tournamentId, + }, + }); - const tournamentData = await executeFetch( - TournamentService.getTournamentByAbbreviation(params.tournamentId), - ); + const { data: tournamentData } = await getTournamentByAbbreviation({ + path: { + abbreviation: params.tournamentId, + }, + }); - if (!tournamentData.status) { -
    {tournamentData.errorMessage}
    ; - } - if (!getStagesData.status) { -
    {getStagesData.errorMessage}
    ; - } + const { data: getQualificationRoomsData } = await getQualificationRooms({ + path: { abbreviation: params.tournamentId }, + }); const getStateTypes = - getStagesData.status && - getStagesData.response.filter((stage) => !!stage.mappool).map((stage) => stage.stageType); + getStagesData && + getStagesData + .filter((stage) => !!stage.mappool) + .filter((stage) => stage.stageType !== "REGISTRATION") + .filter((stage) => stage.stageType !== "SCREENING") + .map((stage) => stage.stageType); const tournamentNavbarRoutes: INavbarProps[] = navbarRoutes.map((item) => { + if ( + getQualificationRoomsData?.length && + getQualificationRoomsData?.length > 0 && + item.name === "Qualification rooms" + ) { + return { + ...item, + href: `/tournament/${params.tournamentId}/qualification-rooms`, + }; + } + if (item.name === "Mappool") { return { ...item, @@ -65,10 +98,7 @@ export default async function Layout({ children, params }: ITournamentLayout) { }; }) as INavbarProps[]; - if ( - tournamentData.status && - tournamentData.response?.tournamentType !== tournamentType.PARTICIPANT_VS - ) { + if (tournamentData && tournamentData?.tournamentType !== tournamentType.PARTICIPANT_VS) { tournamentNavbarRoutes.push({ name: "Teams", href: `/tournament/${params.tournamentId}/teams`, diff --git a/src/app/(tournament)/tournament/[tournamentId]/mappool/[stageType]/page.tsx b/src/app/(tournament)/tournament/[tournamentId]/mappool/[stageType]/page.tsx index d0b6637..7c03bf0 100644 --- a/src/app/(tournament)/tournament/[tournamentId]/mappool/[stageType]/page.tsx +++ b/src/app/(tournament)/tournament/[tournamentId]/mappool/[stageType]/page.tsx @@ -1,13 +1,17 @@ import React from "react"; -import Link from "next/link"; +import { cookies } from "next/headers"; import { type BeatmapModificationResponseDto, - MappoolService, + client, + getMappoolByStage, + getStages, type StageResponseDto, -} from "../../../../../../../generated"; +} from "../../../../../../../client"; import { stageTypeEnumToString } from "@/lib/helpers"; -import { MappoolCard } from "@ui/molecules/Cards/MappoolCard"; +import { BeatmapListItem } from "@ui/molecules/BeatmapListItem/BeatmapListItem"; +import { StageNavigation } from "@ui/organisms/StageNavigation/StageNavigation"; import Section from "@ui/atoms/Section/Section"; +import { Button } from "@ui/atoms/Button/Button"; const SingleTournamentMappool = async ({ params, @@ -21,166 +25,112 @@ const SingleTournamentMappool = async ({ modification: BeatmapModificationResponseDto["modification"]; }; }) => { - let getMappoolByStageData, mappolStages; + const cookie = cookies().get("JWT")?.value; + // configure internal service client + client.setConfig({ + // set default base url for requests + baseUrl: process.env.NEXT_PUBLIC_API_URL, + // set default headers for requests + headers: { + Cookie: `token=${cookie}`, + }, + }); + + let getMappoolByStageData; try { - getMappoolByStageData = await MappoolService.getMappoolByStage( - params.tournamentId, - params.stageType, - ); - - mappolStages = getMappoolByStageData.beatmapsModifications.filter( - (stage) => !!stage.beatmaps, - ); + const { data: getMappoolByStageData1 } = await getMappoolByStage({ + path: { + abbreviation: params.tournamentId, + stageType: params.stageType, + }, + }); + getMappoolByStageData = getMappoolByStageData1; } catch (error) { console.error(error); } - const selectedMappoolByModification = searchParams.modification - ? getMappoolByStageData?.beatmapsModifications.filter( - (m) => m.modification === searchParams.modification, - ) - : getMappoolByStageData?.beatmapsModifications; - - const modifications = mappolStages - ?.filter((m) => !m.isHidden) - ?.filter((m) => m.beatmaps?.length && m.beatmaps?.length > 0) - .map((m) => m.modification); - - const countModificationBeatmaps: - | { - [key: string]: number; - } - | undefined = getMappoolByStageData?.beatmapsModifications.reduce( - (acc, mod) => { - acc[mod.modification] = mod.beatmaps?.length || 0; - return acc; + const { data: getStagesData } = await getStages({ + path: { + abbreviation: params.tournamentId, }, - {} as { [key: string]: number }, - ); + }); - const allModificationsCount = - getMappoolByStageData?.beatmapsModifications?.flatMap((m) => m.beatmaps).length || 0; - - const mappoollCard: React.ReactNode[] = []; - - if (searchParams.modification) { - selectedMappoolByModification && - selectedMappoolByModification[0]?.beatmaps?.forEach((map) => { - mappoollCard.push( - , - ); - }); - } else { - selectedMappoolByModification - ?.flatMap((m) => m.beatmaps) - ?.forEach((map) => { - mappoollCard.push( - , - ); - }); - } - - const stageContent = ( - <> -
    -
    -

    - {stageTypeEnumToString(params.stageType)} -

    - {allModificationsCount > 0 ? ( -
      -
    • - - ALL - {allModificationsCount} - -
    • - {modifications?.map((mod, index) => { - const isActive = searchParams.modification === mod; - if (!countModificationBeatmaps) { - return null; - } - const isMappoolEmpty = countModificationBeatmaps[mod] === 0; - - if (isMappoolEmpty) { - return null; - } - - return ( -
    • - - {mod} - - {countModificationBeatmaps[mod]} - - -
    • - ); - })} -
    - ) : ( - "No mappool beatmaps found." - )} -
    -
    -
    {mappoollCard}
    - - ); + // Collect all beatmaps from all modifications, sorted + const allBeatmaps = getMappoolByStageData?.beatmapsModifications + .flatMap((bm) => + bm?.beatmaps + ?.filter((map) => { + if (!searchParams.modification) { + return true; + } + return map.modification === searchParams.modification; + }) + ?.map((map) => ({ + ...map, + modification: bm.modification, + })) || [], + ) + .sort((a, b) => { + // Sort by modification first, then by position + if (a.modification !== b.modification) { + const modOrder = ["NM", "HD", "HR", "DT", "FM", "TB"]; + const aIndex = modOrder.indexOf(a.modification || ""); + const bIndex = modOrder.indexOf(b.modification || ""); + return aIndex - bIndex; + } + return a.position - b.position; + }) || []; return ( <> + {getStagesData && ( + + )}
    -
    -
    +
    +

    Mappool

    +

    + {stageTypeEnumToString(params.stageType)} +

    + {getMappoolByStageData?.downloadUrl && ( + + )} +
    + +
    + {allBeatmaps.map((map) => ( + + ))}
    - {stageContent}
    ); diff --git a/src/app/(tournament)/tournament/[tournamentId]/page.tsx b/src/app/(tournament)/tournament/[tournamentId]/page.tsx index 3401ffa..a70f353 100644 --- a/src/app/(tournament)/tournament/[tournamentId]/page.tsx +++ b/src/app/(tournament)/tournament/[tournamentId]/page.tsx @@ -5,15 +5,20 @@ import { RiBarChartFill } from "react-icons/ri"; import { LiaLongArrowAltRightSolid } from "react-icons/lia"; import Link from "next/link"; import { format } from "date-fns"; +import { cookies } from "next/headers"; import { - AdminStaffMemberService, - MappoolService, + client, + getMappoolsByTournament, + getQualificationRooms, + getStaffMembers1, + getStages, + getTeamsByTournament, + getTournamentByAbbreviation, type StageResponseDto, - StageService, + type stageType, type TeamResponseDto, - TeamService, - TournamentService, -} from "../../../../../generated"; + type tournamentType, +} from "../../../../../client"; import { TeamCard } from "@ui/molecules/Cards/TeamCard"; import { Socials } from "@ui/organisms/Socials/Socials"; import RegisterToTournamentButton from "@ui/molecules/RegisterToTournamentButton/RegisterToTournamentButton"; @@ -21,7 +26,6 @@ import { ScheduleList } from "@ui/organisms/ScheduleList/ScheduleList"; import { MappoolStages } from "@ui/organisms/MappoolStages/MappoolStages"; import { tournamentTeamShowEnumAvailable } from "@/lib/helpers"; import Section from "@ui/atoms/Section/Section"; -import { executeFetch } from "@/lib/executeFetch"; import StaffMember from "@ui/organisms/StaffMember/StaffMember"; const SingleTournament = async ({ @@ -31,46 +35,61 @@ const SingleTournament = async ({ tournamentId: string; }; }) => { - const [getTournamentByAbbreviation, getStages, getStaffMembers] = await Promise.allSettled([ - TournamentService.getTournamentByAbbreviation(params.tournamentId), - StageService.getStages(params.tournamentId), - AdminStaffMemberService.getStaffMembers1(params.tournamentId), - ]); + const cookie = cookies().get("JWT")?.value; + // configure internal service client + client.setConfig({ + // set default base url for requests + baseUrl: process.env.NEXT_PUBLIC_API_URL, + // set default headers for requests + headers: { + Cookie: `token=${cookie}`, + }, + }); + const { data: getTournamentByAbbreviationData } = await getTournamentByAbbreviation({ + path: { + abbreviation: params.tournamentId, + }, + }); + const { data: getStagesData } = await getStages({ + path: { + abbreviation: params.tournamentId, + }, + }); + const { data: getStaffMembers } = await getStaffMembers1({ + path: { + abbreviation: params.tournamentId, + }, + }); - if (getTournamentByAbbreviation.status === "rejected") { - throw new Error("Tournament not found"); //todo: change to proper error - } - if (getStages.status === "rejected") { - throw new Error("Schedule not found"); //todo: change to proper error - } - if (getStaffMembers.status === "rejected") { - throw new Error("Staff not found"); //todo: change to proper error - } + const { data: getQualificationRoomsData } = await getQualificationRooms({ + path: { abbreviation: params.tournamentId }, + }); let teams: TeamResponseDto[] = []; let teamSize = "1vs1"; // 1vs1 for participants if ( - tournamentTeamShowEnumAvailable.includes(getTournamentByAbbreviation.value?.tournamentType) + tournamentTeamShowEnumAvailable.includes( + getTournamentByAbbreviationData?.tournamentType as tournamentType, + ) ) { - teamSize = `${getTournamentByAbbreviation.value?.minimumTeamSize}vs${getTournamentByAbbreviation.value?.minimumTeamSize}`; - try { - const getTeamsByTournament = await TeamService.getTeamsByTournament( - params.tournamentId, - ); - teams = getTeamsByTournament; - } catch (error) { - throw new Error("Teams not found"); //todo: change to proper error - } - } + teamSize = `${getTournamentByAbbreviationData?.minimumTeamSize}vs${getTournamentByAbbreviationData?.minimumTeamSize}`; - const isStaff = getStaffMembers && getStaffMembers.value.length > 0; + const { data: teamsData } = await getTeamsByTournament({ + path: { + abbreviation: params.tournamentId, + }, + }); + teams = teamsData || []; + } - const mappools = await executeFetch( - MappoolService.getMappoolsByTournament(params.tournamentId), - ); + const { data: mappools } = await getMappoolsByTournament({ + path: { + abbreviation: params.tournamentId, + }, + }); const getStageByStageType = (stageType: string): StageResponseDto => { - return getStages.value.find( + return getStagesData?.find( (stage: StageResponseDto) => stage.stageType === stageType, ) as StageResponseDto; }; @@ -82,9 +101,9 @@ const SingleTournament = async ({

    - {getTournamentByAbbreviation?.value?.name} + {getTournamentByAbbreviationData?.name}

    - {getTournamentByAbbreviation?.value?.isOngoing && ( + {getTournamentByAbbreviationData?.isOngoing && (
    @@ -100,9 +119,11 @@ const SingleTournament = async ({ {/*)}*/} {/*Apply for staff*/} @@ -126,19 +147,18 @@ const SingleTournament = async ({ {" "} {format( - new Date(getTournamentByAbbreviation?.value?.startDate || 0), + new Date(getTournamentByAbbreviationData?.startDate || 0), "dd/MM/yyyy", )}{" "} -{" "} {format( - new Date(getTournamentByAbbreviation?.value?.endDate || 0), + new Date(getTournamentByAbbreviationData?.endDate || 0), "dd/MM/yyyy", )} - {" "} - {getTournamentByAbbreviation.value?.minimumRankLimit || 0} -{" "} - {getTournamentByAbbreviation.value?.maximumRankLimit || 0} + {getTournamentByAbbreviationData?.minimumRankLimit || 0}{" "} + - {getTournamentByAbbreviationData?.maximumRankLimit || 0}
    @@ -202,7 +222,7 @@ const SingleTournament = async ({
    - +
    - {mappools.status && mappools.response.length !== 0 ? ( + {getQualificationRoomsData?.length && getQualificationRoomsData?.length > 0 && ( +
    +
    + +

    + Qualification rooms +

    {" "} + + +
    +
    + )} + {mappools && mappools.length !== 0 ? (

    Mappool

    - {mappools.response.map((mappool) => { + {mappools?.map((mappool) => { const stage = getStageByStageType(mappool.stage); return (
    @@ -234,7 +278,7 @@ const SingleTournament = async ({ start: stage.startDate, end: stage.endDate, }, - stageEnum: stage.stageType, + stageEnum: stage.stageType as stageType, }} mappool={mappool} tournamentAbbreviation={params.tournamentId} @@ -245,54 +289,50 @@ const SingleTournament = async ({
    ) : undefined} - {getTournamentByAbbreviation.value?.tournamentType === "TEAM_VS" && - teams.length > 0 && ( -
    -
    - 0 && ( +
    +
    + +

    -

    - Teams -

    {" "} - - -
    -
    - {teams.map((team, index) => { - if (index > 1) { - return null; + Teams + {" "} + -
    - -
    - - ); - })} -
    -
    - )} + /> + +
    +
    + {teams.map((team, index) => { + if (index > 1) { + return null; + } + return ( + +
    + +
    +
    + ); + })} +
    +
    + )}

    Prizes

    - {getTournamentByAbbreviation.value.prizePool?.map((prize) => ( + {getTournamentByAbbreviationData?.prizePool?.map((prize) => (
    ))}
    - {isStaff && ( -
    -
    - +
    + +

    -

    - Staff -

    {" "} - - -
    -
    - {getStaffMembers.value.map((member) => ( - - ))} -
    -
    - )} + Staff + {" "} + + +
    +
    + {getStaffMembers?.map((member) => ( + + ))} +
    +

    { + return rooms + .map((room) => { + if (room.tournamentType !== tournamentType.PARTICIPANT_VS) { + return room as TeamBasedQualificationRoom; + } else { + return room as ParticipantBasedQualificationRoom; + } + }) + .sort((a, b) => { + return a.startDate > b.startDate ? 1 : -1; + }); +}; + +const QualificationRoomsPage = async ({ + params: { tournamentId }, +}: { + params: { + tournamentId: string; + }; +}) => { + const cookie = cookies().get("JWT")?.value; + // configure internal service client + client.setConfig({ + // set default base url for requests + baseUrl: process.env.NEXT_PUBLIC_API_URL, + // set default headers for requests + headers: { + Cookie: `token=${cookie}`, + }, + }); + const userData = await getUser(); + + const { data: getQualificationRoomsData } = await getQualificationRooms({ + path: { abbreviation: tournamentId }, + }); + + const { data: getTeamFromTournamentData } = await getTeamsByTournament({ + path: { abbreviation: tournamentId }, + }); + + const isUserCaptain = getTeamFromTournamentData?.some( + (team) => team.captain?.user.id === userData?.id, + ); + + const isTeamSignedIn = ( + currentRoom: TeamBasedQualificationRoom | ParticipantBasedQualificationRoom, + ) => { + return ( + currentRoom.tournamentType !== tournamentType.PARTICIPANT_VS && + (currentRoom as TeamBasedQualificationRoom).teams?.some((team) => + team.participants.some((participant) => participant.id === userData?.id), + ) + ); + }; + + return ( +
    +
    +

    Qualification rooms

    + +
    + + + + + + + {isUserCaptain && } + + + + {qualificationRooms(getQualificationRoomsData || []).map((room) => ( + + + + + {isUserCaptain && ( + + )} + + ))} + +
    Start date time (UTC+0)RosterRefereeActions
    {format(new Date(room.startDate), "dd/MM/yyyy HH:mm")} + {room.tournamentType === tournamentType.PARTICIPANT_VS + ? ( + room as ParticipantBasedQualificationRoom + ).participants.map((participant) => ( +
    + {participant.user.username} +
    + )) + : (room as TeamBasedQualificationRoom).teams.map( + (team) => ( +
    + {team.name} + {team.name} +
    + ), + )} +
    + {room.staffMember ? ( +
    +
    +
    + Avatar Tailwind CSS Component +
    +
    + {room.staffMember.user?.username} +
    + ) : ( + "-" + )} +
    + {isUserCaptain && !isTeamSignedIn(room) ? ( +
    { + "use server"; + const cookie = cookies().get("JWT")?.value; + // configure internal service client + client.setConfig({ + // set default base url for requests + baseUrl: + process.env.NEXT_PUBLIC_API_URL, + // set default headers for requests + headers: { + Cookie: `token=${cookie}`, + }, + }); + const { data } = + await signInQualificationRoomAsCaptain({ + path: { + abbreviation: tournamentId, + roomId: room.id, + }, + }); + + console.log(data, "awd"); + await multipleRevalidatePaths([ + "/", + `/dashboard/${tournamentId}/qualification-rooms`, + `/tournament/${tournamentId}/qualification-rooms`, + ]); + }} + > + +
    + ) : ( + "-" + )} +
    +
    +
    +
    + ); +}; + +export default QualificationRoomsPage; diff --git a/src/app/(tournament)/tournament/[tournamentId]/registration/layout.tsx b/src/app/(tournament)/tournament/[tournamentId]/registration/layout.tsx index 07d2e81..533795f 100644 --- a/src/app/(tournament)/tournament/[tournamentId]/registration/layout.tsx +++ b/src/app/(tournament)/tournament/[tournamentId]/registration/layout.tsx @@ -1,5 +1,17 @@ import React from "react"; - +import { cookies } from "next/headers"; +import { client } from "../../../../../../client"; +// todo po co to jest>? export default function MainPageLayout({ children }: { children: React.ReactNode }) { + const cookie = cookies().get("JWT")?.value; + // configure internal service client + client.setConfig({ + // set default base url for requests + baseUrl: process.env.NEXT_PUBLIC_API_URL, + // set default headers for requests + headers: { + Cookie: `token=${cookie}`, + }, + }); return
    {children}
    ; } diff --git a/src/app/(tournament)/tournament/[tournamentId]/registration/page.tsx b/src/app/(tournament)/tournament/[tournamentId]/registration/page.tsx index 59b0321..c0ec038 100644 --- a/src/app/(tournament)/tournament/[tournamentId]/registration/page.tsx +++ b/src/app/(tournament)/tournament/[tournamentId]/registration/page.tsx @@ -64,31 +64,18 @@ const SingleTournamentRegistration = ({ } >
    -

    Topic 1

    +

    Disclaimer

    - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed - malesuada, tortor nec tincidunt tincidunt, libero nunc ullamcorper - nunc, nec interdum turpis libero a libero. Integer quis ex nec purus - lacinia ultricies. Nullam auctor, nisl vel ultricies condimentum, - purus nunc lacinia purus, nec ullamcorper libero ligula nec sapien. - Nunc id semper nunc, nec ullamcorper libero. Nullam auctor, nisl vel - ultricies condimentum, purus nunc lacinia purus, nec ullamcorper - libero ligula nec sapien. Nunc id semper nunc, nec ullamcorper - libero. -

    -
    -
    -

    Topic 2

    -

    - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed - malesuada, tortor nec tincidunt tincidunt, libero nunc ullamcorper - nunc, nec interdum turpis libero a libero. Integer quis ex nec purus - lacinia ultricies. Nullam auctor, nisl vel ultricies condimentum, - purus nunc lacinia purus, nec ullamcorper libero ligula nec sapien. - Nunc id semper nunc, nec ullamcorper libero. Nullam auctor, nisl vel - ultricies condimentum, purus nunc lacinia purus, nec ullamcorper - libero ligula nec sapien. Nunc id semper nunc, nec ullamcorper - libero. + By registering for this tournament, you confirm that you meet the + eligibility requirements and agree to abide by all rules set by the + organizers. Participants are expected to behave respectfully and + fairly; any misconduct, including cheating or harassment, may result + in disqualification. You are responsible for attending matches at + the scheduled times, and failure to do so may lead to forfeiture. + Prizes, if offered, will be awarded as stated, but the organizers + reserve the right to make changes. The organizers are not liable for + any technical issues during the event. By registering, you accept + these terms and conditions.

    diff --git a/src/app/(tournament)/tournament/[tournamentId]/rules/page.tsx b/src/app/(tournament)/tournament/[tournamentId]/rules/page.tsx index 63ef905..616aace 100644 --- a/src/app/(tournament)/tournament/[tournamentId]/rules/page.tsx +++ b/src/app/(tournament)/tournament/[tournamentId]/rules/page.tsx @@ -1,7 +1,7 @@ import React from "react"; -import { TournamentService } from "../../../../../../generated"; -import { executeFetch } from "@/lib/executeFetch"; import "react-quill/dist/quill.snow.css"; +import { cookies } from "next/headers"; +import { client, getTournamentByAbbreviation } from "../../../../../../client"; import Section from "@ui/atoms/Section/Section"; const styles = @@ -23,13 +23,21 @@ const SingleTournamentRules = async ({ tournamentId: string; }; }) => { - const tournament = await executeFetch( - TournamentService.getTournamentByAbbreviation(params.tournamentId), - ); - - if (!tournament.status) { - return
    {tournament.errorMessage}
    ; - } + const cookie = cookies().get("JWT")?.value; + // configure internal service client + client.setConfig({ + // set default base url for requests + baseUrl: process.env.NEXT_PUBLIC_API_URL, + // set default headers for requests + headers: { + Cookie: `token=${cookie}`, + }, + }); + const { data: tournament } = await getTournamentByAbbreviation({ + path: { + abbreviation: params.tournamentId, + }, + }); return (
    @@ -42,65 +50,9 @@ const SingleTournamentRules = async ({
    - {/*
    */} - {/*

    Topic 1

    */} - {/*

    */} - {/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed*/} - {/* malesuada, tortor nec tincidunt tincidunt, libero nunc ullamcorper*/} - {/* nunc, nec interdum turpis libero a libero. Integer quis ex nec purus*/} - {/* lacinia ultricies. Nullam auctor, nisl vel ultricies condimentum,*/} - {/* purus nunc lacinia purus, nec ullamcorper libero ligula nec sapien.*/} - {/* Nunc id semper nunc, nec ullamcorper libero. Nullam auctor, nisl vel*/} - {/* ultricies condimentum, purus nunc lacinia purus, nec ullamcorper*/} - {/* libero ligula nec sapien. Nunc id semper nunc, nec ullamcorper*/} - {/* libero.*/} - {/*

    */} - {/*
    */} - {/*
    */} - {/*

    Topic 2

    */} - {/*

    */} - {/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed*/} - {/* malesuada, tortor nec tincidunt tincidunt, libero nunc ullamcorper*/} - {/* nunc, nec interdum turpis libero a libero. Integer quis ex nec purus*/} - {/* lacinia ultricies. Nullam auctor, nisl vel ultricies condimentum,*/} - {/* purus nunc lacinia purus, nec ullamcorper libero ligula nec sapien.*/} - {/* Nunc id semper nunc, nec ullamcorper libero. Nullam auctor, nisl vel*/} - {/* ultricies condimentum, purus nunc lacinia purus, nec ullamcorper*/} - {/* libero ligula nec sapien. Nunc id semper nunc, nec ullamcorper*/} - {/* libero.*/} - {/*

    */} - {/*
    */} - {/*
    */} - {/*

    Topic 3

    */} - {/*

    */} - {/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed*/} - {/* malesuada, tortor nec tincidunt tincidunt, libero nunc ullamcorper*/} - {/* nunc, nec interdum turpis libero a libero. Integer quis ex nec purus*/} - {/* lacinia ultricies. Nullam auctor, nisl vel ultricies condimentum,*/} - {/* purus nunc lacinia purus, nec ullamcorper libero ligula nec sapien.*/} - {/* Nunc id semper nunc, nec ullamcorper libero. Nullam auctor, nisl vel*/} - {/* ultricies condimentum, purus nunc lacinia purus, nec ullamcorper*/} - {/* libero ligula nec sapien. Nunc id semper nunc, nec ullamcorper*/} - {/* libero.*/} - {/*

    */} - {/*
    */} - {/*
    */} - {/*

    Topic 4

    */} - {/*

    */} - {/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed*/} - {/* malesuada, tortor nec tincidunt tincidunt, libero nunc ullamcorper*/} - {/* nunc, nec interdum turpis libero a libero. Integer quis ex nec purus*/} - {/* lacinia ultricies. Nullam auctor, nisl vel ultricies condimentum,*/} - {/* purus nunc lacinia purus, nec ullamcorper libero ligula nec sapien.*/} - {/* Nunc id semper nunc, nec ullamcorper libero. Nullam auctor, nisl vel*/} - {/* ultricies condimentum, purus nunc lacinia purus, nec ullamcorper*/} - {/* libero ligula nec sapien. Nunc id semper nunc, nec ullamcorper*/} - {/* libero.*/} - {/*

    */} - {/*
    */}
    ); diff --git a/src/app/(tournament)/tournament/[tournamentId]/schedule/page.tsx b/src/app/(tournament)/tournament/[tournamentId]/schedule/page.tsx index 9c4bb2a..fd33d1d 100644 --- a/src/app/(tournament)/tournament/[tournamentId]/schedule/page.tsx +++ b/src/app/(tournament)/tournament/[tournamentId]/schedule/page.tsx @@ -1,6 +1,11 @@ import React from "react"; -import { StageService } from "../../../../../../generated"; -import { ScheduleList } from "@ui/organisms/ScheduleList/ScheduleList"; +import { cookies } from "next/headers"; +import { format } from "date-fns"; +import Link from "next/link"; +import { client, getMatches } from "../../../../../../client"; +import Section from "@ui/atoms/Section/Section"; +import { getUser } from "@/actions/public/getUserAction"; +import { RescheduleMatchModal } from "@/app/(main-page)/(withAuth)/dashboard/[tournamentAbbreviation]/matches/RescheduleMatchModal"; const SingleTournamentSchedule = async ({ params, @@ -9,23 +14,113 @@ const SingleTournamentSchedule = async ({ tournamentId: string; }; }) => { - const data = await StageService.getStages(params.tournamentId); + const cookie = cookies().get("JWT")?.value; + // configure internal service client + client.setConfig({ + // set default base url for requests + baseUrl: process.env.NEXT_PUBLIC_API_URL, + // set default headers for requests + headers: { + Cookie: `token=${cookie}`, + }, + }); + const userData = await getUser(); + const { data } = await getMatches({ + path: { + abbreviation: params.tournamentId, + }, + }); return ( -
    -
    -
    -
    -

    Schedule

    -
    +
    +
    +

    Match schedule

    + +
    + + + + + + + + + + + + + + + + {data + ?.filter((match) => match.matchResult === null) + ?.sort((a, b) => { + return a.startDate > b.startDate ? 1 : -1; + }) + .map((match) => ( + + + + + + + + + + + + ))} + +
    Match IDStart date time (UTC+0)StageTeam blueTeam redRefereeCommentatorsStreamersAction
    {match.matchId} + {format(new Date(match.startDate), "dd/MM/yyyy HH:mm")} + {match.stage?.stageType} + + {match.teamBlue.name} + + + + {match.teamRed.name} + + + {match.referees?.map((referee) => ( +
    + {referee?.user?.username} +
    + ))} +
    + {match.commentators?.map((commentator) => ( +
    + {commentator.user?.username} +
    + ))} +
    + {match.streamers?.map((streamer) => ( +
    + {streamer?.user?.username} +
    + ))} +
    + {(match.teamRed?.captain?.user?.id === userData?.id || + match.teamBlue?.captain?.user?.id === userData?.id) && ( + + )} +
    - -
    -
    + +
    ); }; diff --git a/src/app/(tournament)/tournament/[tournamentId]/staff/page.tsx b/src/app/(tournament)/tournament/[tournamentId]/staff/page.tsx index 7327171..29d6dad 100644 --- a/src/app/(tournament)/tournament/[tournamentId]/staff/page.tsx +++ b/src/app/(tournament)/tournament/[tournamentId]/staff/page.tsx @@ -1,5 +1,6 @@ import React from "react"; -import { StaffMemberService } from "../../../../../../generated"; +import { cookies } from "next/headers"; +import { client, getStaffMembers } from "../../../../../../client"; import StaffMember from "@ui/organisms/StaffMember/StaffMember"; import Section from "@ui/atoms/Section/Section"; @@ -10,9 +11,23 @@ const SingleTournamentStaff = async ({ tournamentId: string; }; }) => { - const getStaffMembers = await StaffMemberService.getStaffMembers(params.tournamentId); + const cookie = cookies().get("JWT")?.value; + // configure internal service client + client.setConfig({ + // set default base url for requests + baseUrl: process.env.NEXT_PUBLIC_API_URL, + // set default headers for requests + headers: { + Cookie: `token=${cookie}`, + }, + }); + const { data: getStaffMembers1 } = await getStaffMembers({ + path: { + abbreviation: params.tournamentId, + }, + }); - const isStaff = getStaffMembers.some( + const isStaff = getStaffMembers1?.some( (staff) => staff.staffMembers && staff.staffMembers.length > 0, ); @@ -26,8 +41,8 @@ const SingleTournamentStaff = async ({ {!isStaff &&

    No staff members

    }
    {isStaff && - getStaffMembers - .sort((a, b) => { + getStaffMembers1 + ?.sort((a, b) => { if (a.position < b.position) { return -1; } diff --git a/src/app/(tournament)/tournament/[tournamentId]/teams/[teamId]/ChangeTeamNameForm.tsx b/src/app/(tournament)/tournament/[tournamentId]/teams/[teamId]/ChangeTeamNameForm.tsx index 3457587..987556a 100644 --- a/src/app/(tournament)/tournament/[tournamentId]/teams/[teamId]/ChangeTeamNameForm.tsx +++ b/src/app/(tournament)/tournament/[tournamentId]/teams/[teamId]/ChangeTeamNameForm.tsx @@ -1,14 +1,17 @@ "use client"; -import React from "react"; +import React, { useState } from "react"; import { toast } from "sonner"; +import Image from "next/image"; +import { useRouter } from "next/navigation"; import { Input } from "@ui/atoms/Forms/Input/Input"; import { Button } from "@ui/atoms/Button/Button"; import { useTypeSafeFormState } from "@/hooks/useTypeSafeFormState"; import { updateTeamSchema } from "@/formSchemas/updateTeamSchema"; -import { updateTeam } from "@/actions/public/createTeamAction"; +import { updateTeamAction } from "@/actions/public/createTeamAction"; export const ChangeTeamNameForm = ({ team: { teamId, teamName, logoUrl, tournamentAbbreviation }, + isRegistrationStage, }: { team: { tournamentAbbreviation: string; @@ -16,19 +19,123 @@ export const ChangeTeamNameForm = ({ teamName: string; logoUrl: string; }; + isRegistrationStage: boolean; }) => { + const router = useRouter(); + const [selectedLogo, setSelectedLogo] = useState(null); + const [logoPreview, setLogoPreview] = useState(null); + const [logoError, setLogoError] = useState(null); + + const validateLogo = (file: File): Promise => { + return new Promise((resolve) => { + // Check file size (max 2MB) + if (file.size > 2 * 1024 * 1024) { + resolve("Image size cannot exceed 2MB"); + return; + } + + // Check file extension + const allowedExtensions = [".jpg", ".jpeg", ".png"]; + const fileName = file.name.toLowerCase(); + const hasValidExtension = allowedExtensions.some(ext => fileName.endsWith(ext)); + if (!hasValidExtension) { + resolve("Image must be in format: jpg, jpeg, or png"); + return; + } + + // Check image dimensions + const img = new window.Image(); + const objectUrl = URL.createObjectURL(file); + img.onload = () => { + URL.revokeObjectURL(objectUrl); + if (img.width > 50 || img.height > 50) { + resolve("Image dimensions cannot exceed 50x50 pixels"); + } else { + resolve(null); + } + }; + img.onerror = () => { + URL.revokeObjectURL(objectUrl); + resolve("Invalid image file"); + }; + img.src = objectUrl; + }); + }; + + const handleLogoChange = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) { + setSelectedLogo(null); + setLogoPreview(null); + setLogoError(null); + return; + } + + setLogoError(null); + + // Validate file + const error = await validateLogo(file); + if (error) { + setLogoError(error); + setSelectedLogo(null); + setLogoPreview(null); + e.target.value = ""; + return; + } + + setSelectedLogo(file); + + // Create preview + const reader = new FileReader(); + reader.onloadend = () => { + setLogoPreview(reader.result as string); + }; + reader.readAsDataURL(file); + }; + const [stateChangeTeamProperties, changeTeamPropertiesFormAction] = useTypeSafeFormState( updateTeamSchema, async (data) => { - const updateTeamResponse = await updateTeam(data); + if (!isRegistrationStage) { + return toast.error("You can only update team information during the registration stage.", { + duration: 3000, + }); + } + + // Create FormData from form data and logo file + const formDataToSend = new FormData(); + formDataToSend.append("tournamentAbbreviation", data.tournamentAbbreviation); + formDataToSend.append("teamId", data.teamId); + formDataToSend.append("name", data.name); + if (selectedLogo) { + formDataToSend.append("logo", selectedLogo); + } + + const updateTeamResponse = await updateTeamAction(formDataToSend); if (!updateTeamResponse.status) { - return toast.error(updateTeamResponse.errorMessage, { + const errorMsg: string = updateTeamResponse.errorMessage || "Failed to update team"; + return toast.error(errorMsg, { duration: 3000, }); } + toast.success("Team updated successfully", { + duration: 3000, + }); + setSelectedLogo(null); + setLogoPreview(null); + setLogoError(null); + router.refresh(); }, ); + if (!isRegistrationStage) { + return ( +
    +

    Team editing is only available during the registration stage.

    +
    + ); + } + return (
    - + + {/* Team Logo */} +
    + + + {logoError && ( +
    {logoError}
    + )} + {(logoPreview || (logoUrl && !selectedLogo && logoUrl !== "/aim_logo.svg")) && ( +
    + Preview: +
    +
    + Team logo +
    +
    +
    + )} +
    + diff --git a/src/app/(tournament)/tournament/[tournamentId]/teams/[teamId]/DeleteParticipantButton.tsx b/src/app/(tournament)/tournament/[tournamentId]/teams/[teamId]/DeleteParticipantButton.tsx new file mode 100644 index 0000000..06250ef --- /dev/null +++ b/src/app/(tournament)/tournament/[tournamentId]/teams/[teamId]/DeleteParticipantButton.tsx @@ -0,0 +1,62 @@ +"use client"; + +import { useTransition } from "react"; +import { useRouter } from "next/navigation"; +import { toast } from "sonner"; +import { deleteParticipantFromTeamAction } from "@/actions/public/deleteParticipantFromTeamAction"; + +interface DeleteParticipantButtonProps { + tournamentId: string; + teamId: string; + participantId: string; + isRegistrationStage: boolean; +} + +export const DeleteParticipantButton = ({ + tournamentId, + teamId, + participantId, + isRegistrationStage, +}: DeleteParticipantButtonProps) => { + const router = useRouter(); + const [isPending, startTransition] = useTransition(); + + const handleDelete = () => { + if (!isRegistrationStage) { + toast.error("You can only delete participants during the registration stage.", { + duration: 3000, + }); + return; + } + + startTransition(async () => { + const result = await deleteParticipantFromTeamAction( + tournamentId, + teamId, + participantId, + ); + if (result.status) { + toast.success("Participant removed successfully", { + duration: 3000, + }); + router.refresh(); + } else { + toast.error(result.errorMessage || "Failed to remove participant", { + duration: 3000, + }); + } + }); + }; + + return ( + + ); +}; + diff --git a/src/app/(tournament)/tournament/[tournamentId]/teams/[teamId]/DisbandTeamButton.tsx b/src/app/(tournament)/tournament/[tournamentId]/teams/[teamId]/DisbandTeamButton.tsx new file mode 100644 index 0000000..e46e6c6 --- /dev/null +++ b/src/app/(tournament)/tournament/[tournamentId]/teams/[teamId]/DisbandTeamButton.tsx @@ -0,0 +1,56 @@ +"use client"; + +import { useTransition } from "react"; +import { useRouter } from "next/navigation"; +import { toast } from "sonner"; +import { Button } from "@ui/atoms/Button/Button"; +import { disbandTeamAction } from "@/actions/public/disbandTeamAction"; + +interface DisbandTeamButtonProps { + tournamentId: string; + teamId: string; + isRegistrationStage: boolean; +} + +export const DisbandTeamButton = ({ + tournamentId, + teamId, + isRegistrationStage, +}: DisbandTeamButtonProps) => { + const router = useRouter(); + const [isPending, startTransition] = useTransition(); + + const handleDisband = () => { + if (!isRegistrationStage) { + toast.error("You can only disband team during the registration stage.", { + duration: 3000, + }); + return; + } + + startTransition(async () => { + const result = await disbandTeamAction(tournamentId, teamId); + if (result.status) { + toast.success("Team disbanded successfully", { + duration: 3000, + }); + router.push(`/tournament/${tournamentId}/teams`); + } else { + toast.error(result.errorMessage || "Failed to disband team", { + duration: 3000, + }); + } + }); + }; + + return ( + + ); +}; + diff --git a/src/app/(tournament)/tournament/[tournamentId]/teams/[teamId]/InvitePlayerToTeamButton.tsx b/src/app/(tournament)/tournament/[tournamentId]/teams/[teamId]/InvitePlayerToTeamButton.tsx index e5d98d3..4bfd377 100644 --- a/src/app/(tournament)/tournament/[tournamentId]/teams/[teamId]/InvitePlayerToTeamButton.tsx +++ b/src/app/(tournament)/tournament/[tournamentId]/teams/[teamId]/InvitePlayerToTeamButton.tsx @@ -4,23 +4,25 @@ import { toast } from "sonner"; import { Input } from "@ui/atoms/Forms/Input/Input"; import { Button } from "@ui/atoms/Button/Button"; import { useTypeSafeFormState } from "@/hooks/useTypeSafeFormState"; -import { inviteToTeam } from "@/actions/public/createTeamAction"; +import { inviteToTeamAction } from "@/actions/public/createTeamAction"; import { inviteToTeamSchema } from "@/formSchemas/inviteToTeamSchema"; import { resetFormValues } from "@/lib/helpers"; export const InvitePlayerToTeamButton = ({ team: { teamId, tournamentAbbreviation }, + isRegistrationStage, }: { team: { tournamentAbbreviation: string; teamId: string; }; + isRegistrationStage: boolean; }) => { const formRef = React.useRef(null); const [stateInviteToTeam, inviteToTeamFormAction] = useTypeSafeFormState( inviteToTeamSchema, async (data) => { - const inviteToTeamResponse = await inviteToTeam(data); + const inviteToTeamResponse = await inviteToTeamAction(data); if (!inviteToTeamResponse.status) { return toast.error(inviteToTeamResponse.errorMessage, { duration: 3000, @@ -34,6 +36,10 @@ export const InvitePlayerToTeamButton = ({ }, ); + if (!isRegistrationStage) { + return null; + } + return ( { + const cookie = cookies().get("JWT")?.value; + // configure internal service client + client.setConfig({ + // set default base url for requests + baseUrl: process.env.NEXT_PUBLIC_API_URL, + // set default headers for requests + headers: { + Cookie: `token=${cookie}`, + }, + }); const userData = await getUser(); - const getTeam = await executeFetch(TeamService.getTeamsById(tournamentId, teamId)); + const { data: getTeam } = await getTeamsById({ + path: { + abbreviation: tournamentId, + teamId: teamId, + }, + }); - if (!getTeam.status) { - return
    {getTeam.errorMessage}
    ; - } + const { data: tournament } = await getTournamentByAbbreviation({ + path: { + abbreviation: tournamentId, + }, + }); - const isCaptain = getTeam.response.captain.user.id === userData?.id; + const isCaptain = getTeam?.captain.user.id === userData?.id; + const isRegistrationStage = tournament?.currentStage === "REGISTRATION"; return (
    - {getTeam.response.logoUrl && ( -
    -
    - team logo -
    +
    +
    + team logo
    - )} -

    {getTeam.response.name}

    - {isCaptain && ( - { - "use server"; - - const disbandTeamResponse = await executeFetch( - TeamService.disbandTeam(tournamentId, teamId), - [ - `/tournament/${tournamentId}/teams`, - `/tournament/${tournamentId}`, - "/", - ], - ); +
    - if (disbandTeamResponse.status) { - redirect(`/tournament/${tournamentId}/teams`); - } - }} - > - - +
    +

    {getTeam?.name}

    +

    Status: {getTeam?.status}

    +
    + {isCaptain && ( + )}
    {isCaptain && ( <> +
    +

    + Once you finish adding your team roster and setting team image, ping + Host of the tournament to change your team status to ACCEPTED. Any + changes in the team will change its status to PENDING. +

    +
    )} - +
    + Captain discord: + {getTeam?.captain?.user?.discordUsername} +
    Team Members:
    @@ -99,7 +124,7 @@ const TeamPage = async ({ - {getTeam.response.participants.map((participant) => ( + {getTeam?.participants.map((participant) => ( )} diff --git a/src/app/(tournament)/tournament/[tournamentId]/teams/page.tsx b/src/app/(tournament)/tournament/[tournamentId]/teams/page.tsx index aa6eeb0..5101283 100644 --- a/src/app/(tournament)/tournament/[tournamentId]/teams/page.tsx +++ b/src/app/(tournament)/tournament/[tournamentId]/teams/page.tsx @@ -1,7 +1,7 @@ import React from "react"; -import { TeamService } from "../../../../../../generated"; +import { cookies } from "next/headers"; +import { client, getTeamsByTournament } from "../../../../../../client"; import { TeamCard } from "@ui/molecules/Cards/TeamCard"; -import { executeFetch } from "@/lib/executeFetch"; import Section from "@ui/atoms/Section/Section"; import { getUser } from "@/actions/public/getUserAction"; @@ -12,13 +12,24 @@ const SingleTournamentTeams = async ({ tournamentId: string; }; }) => { - const getTeams = await executeFetch(TeamService.getTeamsByTournament(tournamentId)); + const cookie = cookies().get("JWT")?.value; + // configure internal service client + client.setConfig({ + // set default base url for requests + baseUrl: process.env.NEXT_PUBLIC_API_URL, + // set default headers for requests + headers: { + Cookie: `token=${cookie}`, + }, + }); + const { data: getTeams } = await getTeamsByTournament({ + path: { + abbreviation: tournamentId, + }, + }); const userData = await getUser(); - if (!getTeams.status) { - return
    {getTeams.errorMessage}
    ; - } - const findMyTeam = getTeams.response.find((team) => + const findMyTeam = getTeams?.find((team) => team.participants.some((participant) => participant.user.id === userData?.id), ); @@ -43,9 +54,17 @@ const SingleTournamentTeams = async ({
    - {getTeams.response.map((team) => ( - - ))} + {getTeams + ?.sort((a, b) => { + // sort array by averagePerformancePoints, descending + return ( + (b?.averagePerformancePoints || 0) - + (a?.averagePerformancePoints || 0) + ); + }) + .map((team) => ( + + ))}
    diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 586b169..1baf20b 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -2,6 +2,8 @@ import "./globals.css"; import type { Metadata } from "next"; import { Montserrat } from "@next/font/google"; import { Toaster } from "sonner"; +import { cookies } from "next/headers"; +import { client } from "../../client"; export const metadata: Metadata = { title: "AimCup", @@ -13,7 +15,22 @@ const monserrat = Montserrat({ subsets: ["latin"], }); +// configure internal service client +client.setConfig({ + // set default base url for requests + baseUrl: process.env.NEXT_PUBLIC_API_URL || "http://localhost:8080", + // set default headers for requests +}); + export default async function RootLayout({ children }: { children: React.ReactNode }) { + client.interceptors.request.use((req) => { + const cookie = cookies().get("JWT")?.value; + if (cookie) { + req.headers.set("Cookie", `token=${cookie}`); + } + return req; + }); + return ( ; diff --git a/src/formSchemas/editMatchSchema.ts b/src/formSchemas/editMatchSchema.ts new file mode 100644 index 0000000..d7b06a6 --- /dev/null +++ b/src/formSchemas/editMatchSchema.ts @@ -0,0 +1,14 @@ +import * as zod from "zod"; + +export const editMatchSchema = zod.object({ + matchId: zod.string(), + tournamentAbbreviation: zod.string(), + dataTimeStart: zod.string(), + stageType: zod.string(), + refereeIds: zod.array(zod.string()).optional(), + commentatorIds: zod.array(zod.string()).optional(), + streamerIds: zod.array(zod.string()).optional(), +}); + +export type EditMatchSchemaType = zod.infer; + diff --git a/src/formSchemas/rescheduleMatchSchema.ts b/src/formSchemas/rescheduleMatchSchema.ts new file mode 100644 index 0000000..b17933a --- /dev/null +++ b/src/formSchemas/rescheduleMatchSchema.ts @@ -0,0 +1,22 @@ +import * as zod from "zod"; + +export const rescheduleMatchSchema = zod.object({ + matchId: zod.string().uuid(), + proposedDateTime: zod.string().min(1, "Date and time is required"), + tournamentAbbreviation: zod.string(), + agreeToRules: zod + .preprocess( + (val) => { + // Convert checkbox value ("on" or undefined) to boolean + if (val === "on" || val === true) return true; + if (val === undefined || val === null || val === false) return false; + return Boolean(val); + }, + zod.boolean().refine((val) => val === true, { + message: "You must agree to the reschedule rules", + }), + ), +}); + +export type RescheduleMatchSchemaType = zod.infer; + diff --git a/src/formSchemas/updateTeamSchema.ts b/src/formSchemas/updateTeamSchema.ts index 16d1487..cd44d14 100644 --- a/src/formSchemas/updateTeamSchema.ts +++ b/src/formSchemas/updateTeamSchema.ts @@ -1,10 +1,9 @@ import * as zod from "zod"; export const updateTeamSchema = zod.object({ - logoUrl: zod.string(), - name: zod.string(), - teamId: zod.string(), - tournamentAbbreviation: zod.string(), + name: zod.string().min(1, "Team name is required"), + teamId: zod.string().uuid("Invalid team ID"), + tournamentAbbreviation: zod.string().min(1, "Tournament abbreviation is required"), }); export type updateTeamSchemaType = zod.infer; diff --git a/src/lib/executeFetch.ts b/src/lib/executeFetch.ts deleted file mode 100644 index c12c2f6..0000000 --- a/src/lib/executeFetch.ts +++ /dev/null @@ -1,51 +0,0 @@ -"use server"; - -import { revalidatePath } from "next/cache"; -import { type ApiError, OpenAPI } from "../../generated"; -import { verifySession } from "@/lib/session"; - -export type SuccessfulResponse = { - status: true; - response: T; -}; - -export type ErrorResponse = { - status: false; - errorMessage: string; -}; - -export type ApiErrorMessages = { - errors: string[]; -}; - -export type Response = SuccessfulResponse | ErrorResponse; - -export const executeFetch = async ( - fetchRequest: Promise, - revalidatePaths?: string[], -): Promise> => { - "use server"; - const JWT = await verifySession(); - - if (typeof JWT.token === "string") { - OpenAPI.HEADERS = { - Cookie: `token=${JWT.token}`, - }; - } - - return fetchRequest - .then((res) => { - if (revalidatePaths) { - revalidatePaths.forEach((path) => { - revalidatePath(path); - }); - } - return { status: true, response: res } as SuccessfulResponse; - }) - .catch((error) => { - return { - status: false, - errorMessage: ((error as ApiError).body as ApiErrorMessages).errors[0], - } as ErrorResponse; - }); -}; diff --git a/src/lib/helpers.ts b/src/lib/helpers.ts index 0d9d4d8..97c2c36 100644 --- a/src/lib/helpers.ts +++ b/src/lib/helpers.ts @@ -1,31 +1,30 @@ import { type MutableRefObject } from "react"; import { type z, type ZodObject, type ZodRawShape } from "zod"; -import { StageResponseDto, TournamentRequestDto } from "../../generated"; -import tournamentType = TournamentRequestDto.tournamentType; +import { type StageResponseDto, tournamentType } from "../../client"; export const stageTypeEnumToString = (stageType: StageResponseDto["stageType"]) => { switch (stageType) { - case StageResponseDto.stageType.REGISTRATION: + case "REGISTRATION": return "Registration"; - case StageResponseDto.stageType.QUALIFICATION: + case "QUALIFICATION": return "Qualification"; - case StageResponseDto.stageType.FINAL: + case "FINAL": return "Final"; - case StageResponseDto.stageType.RO16: + case "RO16": return "Round of 16"; - case StageResponseDto.stageType.RO64: + case "RO64": return "Round of 64"; - case StageResponseDto.stageType.GRAND_FINAL: + case "GRAND_FINAL": return "Grand Final"; - case StageResponseDto.stageType.QUARTER_FINAL: + case "QUARTER_FINAL": return "Quarter Final"; - case StageResponseDto.stageType.RO32: + case "RO32": return "Round of 32"; - case StageResponseDto.stageType.RO128: + case "RO128": return "Round of 128"; - case StageResponseDto.stageType.SEMI_FINAL: + case "SEMI_FINAL": return "Semi Final"; - case StageResponseDto.stageType.SCREENING: + case "SCREENING": return "Screening"; default: return "Unknown"; @@ -59,7 +58,11 @@ export const resetFormValues = >({ schemaKeys.includes(input.name as keyof z.infer) && !resetWithoutInputNames.includes(input.name as keyof z.infer) ) { - input.value = ""; + if (input.type === "checkbox") { + (input as HTMLInputElement).checked = false; + } else { + input.value = ""; + } } }); }; diff --git a/src/lib/multipleRevalidatePaths.ts b/src/lib/multipleRevalidatePaths.ts new file mode 100644 index 0000000..490a29f --- /dev/null +++ b/src/lib/multipleRevalidatePaths.ts @@ -0,0 +1,9 @@ +"use server"; +import { revalidatePath } from "next/cache"; + +export const multipleRevalidatePaths = async (paths: string[]) => { + "use server"; + paths.forEach((path) => { + revalidatePath(path); + }); +}; diff --git a/src/lib/restClient.ts b/src/lib/restClient.ts new file mode 100644 index 0000000..428c254 --- /dev/null +++ b/src/lib/restClient.ts @@ -0,0 +1,5 @@ +import { client } from "../../client"; + +client.setConfig({ + baseUrl: process.env.NEXT_PUBLIC_API_URL || "http://localhost:8080", +}); diff --git a/src/lib/session.ts b/src/lib/session.ts index 7ad5076..1faa046 100644 --- a/src/lib/session.ts +++ b/src/lib/session.ts @@ -1,40 +1,40 @@ import "server-only"; -import { SignJWT, jwtVerify } from "jose"; +// import { SignJWT, jwtVerify } from "jose"; import { cookies } from "next/headers"; import { redirect } from "next/navigation"; -const secretKey = process.env.SECRET_LOGIN_KEY; -const key = new TextEncoder().encode(secretKey); +// const secretKey = process.env.SECRET_LOGIN_KEY; +// const key = new TextEncoder().encode(secretKey); +// +// type SessionPayload = { +// token: string | number; +// expiresAt: Date; +// }; -type SessionPayload = { - token: string | number; - expiresAt: Date; -}; - -export async function encrypt(payload: SessionPayload) { - return new SignJWT(payload) - .setProtectedHeader({ alg: "HS256" }) - .setIssuedAt() - .setExpirationTime("7d") - .sign(key); -} - -export async function decrypt(session: string | undefined = "") { - try { - const { payload } = await jwtVerify(session, key, { - algorithms: ["HS256"], - }); - return payload; - } catch (error) { - return null; - } -} +// export async function encrypt(payload: SessionPayload) { +// return new SignJWT(payload) +// .setProtectedHeader({ alg: "HS256" }) +// .setIssuedAt() +// .setExpirationTime("7d") +// .sign(key); +// } +// +// export async function decrypt(session: string | undefined = "") { +// try { +// const { payload } = await jwtVerify(session, key, { +// algorithms: ["HS256"], +// }); +// return payload; +// } catch (error) { +// return null; +// } +// } export async function createSession(token: string, redirectUrl: string) { const expiresAt = new Date(Date.now() + 86400000); - const session = await encrypt({ token, expiresAt }); + // const session = await encrypt({ token, expiresAt }); - cookies().set("JWT", session, { + cookies().set("JWT", token, { httpOnly: true, secure: true, expires: expiresAt, @@ -47,20 +47,20 @@ export async function createSession(token: string, redirectUrl: string) { export async function verifySession() { const cookie = cookies().get("JWT")?.value; - const session = await decrypt(cookie); + // const session = await decrypt(cookie); - if (!session?.token) { + if (!cookie) { return { isAuth: false, token: null }; } - return { isAuth: true, token: session.token }; + return { isAuth: true, token: cookie }; } export async function updateSession() { const session = cookies().get("JWT")?.value; - const payload = await decrypt(session); + // const payload = await decrypt(session); - if (!session || !payload) { + if (!session) { return null; } diff --git a/src/middleware.ts b/src/middleware.ts index 7fb479f..99e3399 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -1,6 +1,5 @@ import { type NextRequest, NextResponse } from "next/server"; import { cookies } from "next/headers"; -import { decrypt } from "@/lib/session"; // 1. Specify protected and public routes const protectedRoutes = ["/dashboard", "/account"]; @@ -14,14 +13,14 @@ export default async function middleware(req: NextRequest) { // 3. Decrypt the session from the cookie const cookie = cookies().get("JWT")?.value; - const session = await decrypt(cookie); + // const session = await decrypt(cookie); // 4. Redirect - if (isProtectedRoute && !session?.token) { + if (isProtectedRoute && !cookie) { return NextResponse.redirect(new URL("/register", req.nextUrl)); } - if (isPublicRoute && session?.token && !req.nextUrl.pathname.startsWith("/")) { + if (isPublicRoute && cookie && !req.nextUrl.pathname.startsWith("/")) { return NextResponse.redirect(new URL("/", req.nextUrl)); } diff --git a/src/models/QualificationRoom.ts b/src/models/QualificationRoom.ts new file mode 100644 index 0000000..71ea97b --- /dev/null +++ b/src/models/QualificationRoom.ts @@ -0,0 +1,24 @@ +import type { + ParticipantResponseDto, + QualificationRoomResponseDto, + StaffMemberResponseDto, + TeamResponseDto, +} from "../../client"; + +interface QualificationRoom extends QualificationRoomResponseDto { + id: string; + number: number; + isClosed: number; + maxSlots: number; + occupiedSlots: number; + staffMember: StaffMemberResponseDto; + startDate: string; +} + +export interface TeamBasedQualificationRoom extends QualificationRoom { + teams: TeamResponseDto[]; +} + +export interface ParticipantBasedQualificationRoom extends QualificationRoom { + participants: ParticipantResponseDto[]; +} diff --git a/src/ui/atoms/AvatarGroup/AvatarGroup.tsx b/src/ui/atoms/AvatarGroup/AvatarGroup.tsx new file mode 100644 index 0000000..2584ef9 --- /dev/null +++ b/src/ui/atoms/AvatarGroup/AvatarGroup.tsx @@ -0,0 +1,60 @@ +"use client"; + +import React from "react"; +import Image from "next/image"; +import type { ParticipantResponseDto } from "../../../../client"; + +interface AvatarGroupProps { + participants: ParticipantResponseDto[]; + max?: number; +} + +export const AvatarGroup = ({ + participants, + max = 4, +}: AvatarGroupProps) => { + if (participants.length === 0) { + return null; + } + + const visibleParticipants = participants.slice(0, max); + const remainingCount = participants.length - max; + + return ( +
    + {visibleParticipants.map((participant, index) => ( +
    +
    + +
    +
    + ))} + {remainingCount > 0 && ( +
    +
    + +{remainingCount} +
    +
    + )} +
    + ); +}; + diff --git a/src/ui/atoms/Forms/Select/StaffMemberAutocomplete.tsx b/src/ui/atoms/Forms/Select/StaffMemberAutocomplete.tsx new file mode 100644 index 0000000..2e197e6 --- /dev/null +++ b/src/ui/atoms/Forms/Select/StaffMemberAutocomplete.tsx @@ -0,0 +1,207 @@ +"use client"; +import React, { useState, useEffect, useRef } from "react"; +import { getStaffMembersAction } from "@/actions/public/getStaffMembersAction"; + +export interface selectOptions { + id: string; + label: string; +} + +interface IStaffMemberAutocompleteProps { + name: string; + label: string; + tournamentAbbreviation: string; + selectedOption?: string[]; + errorMessage?: string; + required?: boolean; +} + +export const StaffMemberAutocomplete = ({ + name, + label, + tournamentAbbreviation, + selectedOption = [], + errorMessage, + required = false, +}: IStaffMemberAutocompleteProps) => { + const [isOpen, setIsOpen] = useState(false); + const [options, setOptions] = useState([]); + const [loading, setLoading] = useState(false); + const [searchTerm, setSearchTerm] = useState(""); + const dropdownRef = useRef(null); + const inputRef = useRef(null); + const [selectedLabelsMap, setSelectedLabelsMap] = useState>(new Map()); + const [selectedIds, setSelectedIds] = useState(selectedOption); + + // Sync with prop changes + useEffect(() => { + setSelectedIds(selectedOption); + }, [selectedOption]); + + // Load data when dropdown opens or if there are selected options on mount + useEffect(() => { + if (selectedOption.length > 0 && options.length === 0 && !loading) { + // Load data immediately if there are selected options + void loadStaffMembers(); + } else if (isOpen && options.length === 0 && !loading) { + void loadStaffMembers(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isOpen, selectedOption.length]); + + // Update selected labels map when options are loaded + useEffect(() => { + if (options.length > 0) { + const newMap = new Map(); + options.forEach((opt) => { + if (selectedIds.includes(opt.id)) { + newMap.set(opt.id, opt.label); + } + }); + setSelectedLabelsMap(newMap); + } + }, [options, selectedIds]); + + // Close dropdown when clicking outside + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + const target = event.target as Node; + if ( + dropdownRef.current && + !dropdownRef.current.contains(target) && + inputRef.current && + !inputRef.current.contains(target) + ) { + setIsOpen(false); + setSearchTerm(""); + } + }; + + if (isOpen) { + document.addEventListener("mousedown", handleClickOutside); + } + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, [isOpen]); + + const loadStaffMembers = async () => { + if (loading) return; // Prevent multiple simultaneous loads + setLoading(true); + try { + const staffMemberOptions = await getStaffMembersAction(tournamentAbbreviation); + setOptions(staffMemberOptions); + } catch (error) { + console.error("Error loading staff members:", error); + } finally { + setLoading(false); + } + }; + + const toggleSelection = (optionId: string) => { + const newSelection = selectedIds.includes(optionId) + ? selectedIds.filter((id) => id !== optionId) + : [...selectedIds, optionId]; + setSelectedIds(newSelection); + }; + + const filteredOptions = options.filter((option) => + option.label.toLowerCase().includes(searchTerm.toLowerCase()), + ); + + // Get selected labels from map or from options + const selectedLabels = Array.from(selectedLabelsMap.values()).join(", ") || + options + .filter((opt) => selectedIds.includes(opt.id)) + .map((opt) => opt.label) + .join(", "); + + return ( +
    + +
    + { + setSearchTerm(e.target.value); + setIsOpen(true); + }} + onFocus={() => { + setIsOpen(true); + setSearchTerm(""); + }} + onBlur={(e) => { + // Don't close if clicking on dropdown + if (!dropdownRef.current?.contains(e.relatedTarget as Node)) { + setSearchTerm(""); + } + }} + /> + {isOpen && ( +
    +
    + {loading ? ( +
    + +

    Loading staff members...

    +
    + ) : filteredOptions.length === 0 ? ( +
    + No staff members found +
    + ) : ( + filteredOptions.map((option) => { + const isSelected = selectedIds.includes(option.id); + return ( + + ); + }) + )} +
    +
    + )} +
    + {/* Hidden inputs for form submission */} + {selectedIds.map((id) => ( + + ))} +
    + {errorMessage} +
    +
    + ); +}; + diff --git a/src/ui/molecules/BeatmapListItem/BeatmapListItem.tsx b/src/ui/molecules/BeatmapListItem/BeatmapListItem.tsx new file mode 100644 index 0000000..893665e --- /dev/null +++ b/src/ui/molecules/BeatmapListItem/BeatmapListItem.tsx @@ -0,0 +1,214 @@ +"use client"; + +import React from "react"; +import Image from "next/image"; +import Link from "next/link"; +import { type BeatmapModificationResponseDto } from "../../../../client"; + +interface BeatmapListItemProps { + href: string; + title: string; + artist: string; + version: string; + creator: string; + modification?: BeatmapModificationResponseDto["modification"]; + position: number; + isCustom: boolean; + img?: string; + mapInformation: { + stars: number; + time: number; + bpm: number; + ar: number; + hp: number; + od: number; + cs: number; + }; + _tournamentAbbreviation: string; + _beatmapId: number; + _beatmapsetId: number; +} + +export const BeatmapListItem = ({ + href, + title, + artist, + version, + creator, + modification, + position, + isCustom, + img, + mapInformation, +}: BeatmapListItemProps) => { + const formatTime = (seconds: number) => { + const minutes = Math.floor(seconds / 60); + const remainingSeconds = seconds % 60; + const formattedSeconds = remainingSeconds < 10 ? "0" + remainingSeconds : remainingSeconds; + return `${minutes}:${formattedSeconds}`; + }; + + const getModificationColor = (mod?: string) => { + switch (mod) { + case "NM": + return "bg-blue-600"; + case "HD": + return "bg-yellow-500"; + case "HR": + return "bg-orange-500"; + case "DT": + return "bg-purple-500"; + case "FM": + return "bg-pink-500"; + case "TB": + return "bg-red-500"; + default: + return "bg-gray-600"; + } + }; + + const getModificationLabel = (mod?: string, pos?: number) => { + if (!mod) return ""; + if (mod === "TB") return "TB"; + return `${mod}${pos || ""}`; + }; + + const getModificationGradient = (mod?: string) => { + switch (mod) { + case "NM": + return "transparent"; + case "DT": + return "linear-gradient(to right, rgba(75, 0, 130, 0.3), rgba(75, 0, 130, 0.1), transparent)"; + case "HD": + return "linear-gradient(to right, rgba(255, 215, 0, 0.3), rgba(255, 215, 0, 0.1), transparent)"; + case "HR": + return "linear-gradient(to right, rgba(220, 20, 60, 0.3), rgba(220, 20, 60, 0.1), transparent)"; + case "TB": + return "linear-gradient(to right, rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0.2), transparent)"; + case "FM": + return "linear-gradient(to right, rgba(0, 255, 255, 0.3), rgba(0, 255, 255, 0.1), transparent)"; + case "FL": + return "linear-gradient(to right, rgba(128, 128, 128, 0.3), rgba(128, 128, 128, 0.1), transparent)"; + case "EZ": + return "linear-gradient(to right, rgba(0, 128, 0, 0.3), rgba(0, 128, 0, 0.1), transparent)"; + case "HT": + return "linear-gradient(to right, rgba(255, 140, 0, 0.3), rgba(255, 140, 0, 0.1), transparent)"; + default: + return "transparent"; + } + }; + + return ( +
    + {/* Gradient overlay based on modification - starts from right edge of image */} +
    + + {/* Left section with background image */} + e.stopPropagation()} + className="relative flex-shrink-0 w-64 overflow-hidden rounded-l-lg" + > + + + + {/* Center section with song info */} +
    +

    + {title} +

    +

    + [{version}] +

    +

    + {artist} +

    +

    + Mapped by {creator} +

    +
    + + {/* Custom beatmap logo background - between title and modification badge */} + {isCustom && ( +
    + )} + + {/* Right section with map details */} +
    + {/* Modification badge */} + {modification && ( +
    + {getModificationLabel(modification, modification !== "TB" ? position : undefined)} +
    + )} + + {/* Difficulty info */} +
    + {/* Star rating */} +
    + {mapInformation.stars.toFixed(1)}★ +
    + {/* Matryca 3x2 */} +
    + {/* Kolumna 1: length nad BPM */} + + {formatTime(mapInformation.time)} + + {/* Kolumna 2: AR nad HP */} + + AR {mapInformation.ar.toFixed(1)} + + {/* Kolumna 3: CS nad OD */} + + CS {mapInformation.cs.toFixed(1)} + + {/* Kolumna 1, wiersz 2: BPM pod length */} + + {mapInformation.bpm.toFixed(0)} bpm + + {/* Kolumna 2, wiersz 2: HP pod AR */} + + HP {mapInformation.hp.toFixed(1)} + + {/* Kolumna 3, wiersz 2: OD pod CS */} + + OD {mapInformation.od.toFixed(1)} + +
    +
    +
    +
    + ); +}; + diff --git a/src/ui/molecules/Cards/MappoolCard.tsx b/src/ui/molecules/Cards/MappoolCard.tsx index 04ba374..aa793a1 100644 --- a/src/ui/molecules/Cards/MappoolCard.tsx +++ b/src/ui/molecules/Cards/MappoolCard.tsx @@ -3,19 +3,23 @@ import Image from "next/image"; import { IoTime } from "react-icons/io5"; import { IoIosStar } from "react-icons/io"; import { PiMetronomeFill } from "react-icons/pi"; -import { type BeatmapModificationResponseDto } from "../../../../generated"; +import { type BeatmapModificationResponseDto } from "../../../../client"; import { TextBox } from "@ui/atoms/TextBox/TextBox"; export const MappoolCard = ({ title, + version, modification, isCustom, img, author, mapInformation, position, + href, }: { + href: string; title?: string; + version?: string; modification?: BeatmapModificationResponseDto["modification"]; isCustom: boolean; img?: string; @@ -31,153 +35,108 @@ export const MappoolCard = ({ cs: number; }; }) => { - return ( -
    -
    - - - - {isCustom && ( -
    - Custom -
    - )} + const formatTime = (seconds: number) => { + const minutes = Math.floor(seconds / 60); + const remainingSeconds = seconds % 60; -
    @@ -123,34 +148,12 @@ const TeamPage = async ({ {isCaptain && (
    {participant.user.id !== userData?.id && ( // if not captain -
    { - "use server"; - - await executeFetch( - TeamService.deleteParticipantFromTeam( - tournamentId, - teamId, - "" + participant.user.osuId, - ), - [ - "/tournament/[tournamentId]/teams", - "/tournament", - "/", - ], - ); - }} - > - -
    + )}