From 7f02bd10b51c8fc3867160b8815d321ef7b7278b Mon Sep 17 00:00:00 2001 From: bohdancho Date: Sat, 23 Dec 2023 16:51:38 +0100 Subject: [PATCH 01/27] refactor: migrate to tanstack-router, disable search param for reconstructions --- bun.lockb | Bin 134912 -> 137776 bytes package-lock.json | 3687 ----------------- package.json | 3 +- src/App.tsx | 143 +- src/api/contests/useContestResults.ts | 4 +- src/components/DevResetSession.tsx | 4 +- src/components/DisciplinesTabsLayout.tsx | 6 +- src/components/layout/Layout.tsx | 2 +- src/components/layout/components/Navbar.tsx | 21 +- src/constants.ts | 3 - src/integrations/cube/Cube.tsx | 2 +- src/loaders/index.ts | 2 - src/loaders/redirectToDefaultDiscipline.ts | 12 - src/loaders/redirectToOngoingContest.ts | 7 - src/pages/contest/ContestDiscipline.tsx | 37 +- .../contest/components/PublishedSession.tsx | 6 +- .../SolveContest/components/CurrentSolve.tsx | 6 +- .../components/SubmittedSolve.tsx | 6 +- src/pages/dashboard/components/BestSolves.tsx | 8 +- .../dashboard/components/ContestsList.tsx | 7 +- .../leaderboard/LeaderboardDiscipline.tsx | 14 +- src/types.ts | 4 + src/utils/index.ts | 1 - src/utils/useRequiredParams.ts | 5 - 24 files changed, 155 insertions(+), 3835 deletions(-) delete mode 100644 package-lock.json delete mode 100644 src/constants.ts delete mode 100644 src/loaders/index.ts delete mode 100644 src/loaders/redirectToDefaultDiscipline.ts delete mode 100644 src/loaders/redirectToOngoingContest.ts delete mode 100644 src/utils/useRequiredParams.ts diff --git a/bun.lockb b/bun.lockb index e5a8b4da584472faaa729723bb13fa5d197dacec..229059279ff13ecc2aaa9337688af76231fe44ca 100755 GIT binary patch delta 31509 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 delta 29873 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.10.0" - } - }, - "node_modules/@alloc/quick-lru": { - "version": "5.2.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", - "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", - "dev": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.22.13", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", - "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", - "dev": true, - "dependencies": { - "@babel/highlight": "^7.22.13", - "chalk": "^2.4.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/code-frame/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/code-frame/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/@babel/code-frame/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@babel/code-frame/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.2.tgz", - "integrity": "sha512-0S9TQMmDHlqAZ2ITT95irXKfxN9bncq8ZCoJhun3nHL/lLUxd2NKBJYoNGWH7S0hz6fRQwWlAWn/ILM0C70KZQ==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.2.tgz", - "integrity": "sha512-n7s51eWdaWZ3vGT2tD4T7J6eJs3QoBXydv7vkUM06Bf1cbVD2Kc2UrkzhiQwobfV7NwOnQXYL7UBJ5VPU+RGoQ==", - "dev": true, - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.22.13", - "@babel/generator": "^7.23.0", - "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-module-transforms": "^7.23.0", - "@babel/helpers": "^7.23.2", - "@babel/parser": "^7.23.0", - "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.2", - "@babel/types": "^7.23.0", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/generator": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", - "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", - "dev": true, - "dependencies": { - "@babel/types": "^7.23.0", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", - "jsesc": "^2.5.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz", - "integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==", - "dev": true, - "dependencies": { - "@babel/compat-data": "^7.22.9", - "@babel/helper-validator-option": "^7.22.15", - "browserslist": "^4.21.9", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true - }, - "node_modules/@babel/helper-environment-visitor": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", - "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-function-name": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", - "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", - "dev": true, - "dependencies": { - "@babel/template": "^7.22.15", - "@babel/types": "^7.23.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-hoist-variables": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", - "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", - "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.15" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.0.tgz", - "integrity": "sha512-WhDWw1tdrlT0gMgUJSlX0IQvoO1eN279zrAUbVB+KpV2c3Tylz8+GnKOLllCS6Z/iZQEyVYxhZVUdPTqs2YYPw==", - "dev": true, - "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-module-imports": "^7.22.15", - "@babel/helper-simple-access": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/helper-validator-identifier": "^7.22.20" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-simple-access": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", - "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-split-export-declaration": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", - "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", - "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz", - "integrity": "sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.2.tgz", - "integrity": "sha512-lzchcp8SjTSVe/fPmLwtWVBFC7+Tbn8LGHDVfDp9JGxpAY5opSaEFgt8UQvrnECWOTdji2mOWMz1rOhkHscmGQ==", - "dev": true, - "dependencies": { - "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.2", - "@babel/types": "^7.23.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", - "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.22.20", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/highlight/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/@babel/highlight/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@babel/highlight/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/parser": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", - "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", - "dev": true, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/runtime": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.5.tgz", - "integrity": "sha512-NdUTHcPe4C99WxPub+K9l9tK5/lV4UXIoaHSYgzco9BCyjKAAwzdBI+wWtYqHt7LJdbo74ZjRPJgzVweq1sz0w==", - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/template": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", - "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/parser": "^7.22.15", - "@babel/types": "^7.22.15" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", - "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/generator": "^7.23.0", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.23.0", - "@babel/types": "^7.23.0", - "debug": "^4.1.0", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/types": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", - "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", - "dev": true, - "dependencies": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.18.20", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.8.1", - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.2", - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/js": { - "version": "8.50.0", - "license": "MIT", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.11.11", - "license": "Apache-2.0", - "dependencies": { - "@humanwhocodes/object-schema": "^1.2.1", - "debug": "^4.1.1", - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=10.10.0" - } - }, - "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { - "version": "3.0.8", - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/object-schema": { - "version": "1.2.1", - "license": "BSD-3-Clause" - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.3", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.19", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@react-oauth/google": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/@react-oauth/google/-/google-0.11.1.tgz", - "integrity": "sha512-tywZisXbsdaRBVbEu0VX6dRbOSL2I6DgY97woq5NMOOOz+xtDsm418vqq+Vx10KMtra3kdHMRMf0hXLWrk2RMg==", - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@remix-run/router": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.10.0.tgz", - "integrity": "sha512-Lm+fYpMfZoEucJ7cMxgt4dYt8jLfbpwRCzAjm9UgSLOkmlqo9gupxt6YX3DY0Fk155NT9l17d/ydi+964uS9Lw==", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@rollup/pluginutils": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.5.tgz", - "integrity": "sha512-6aEYR910NyP73oHiJglti74iRyOwgFU4x3meH/H8OJx6Ry0j6cOVZ5X/wTvub7G7Ao6qaHBEaNsV3GLJkSsF+Q==", - "dev": true, - "dependencies": { - "@types/estree": "^1.0.0", - "estree-walker": "^2.0.2", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@svgr/babel-plugin-add-jsx-attribute": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz", - "integrity": "sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==", - "dev": true, - "engines": { - "node": ">=14" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@svgr/babel-plugin-remove-jsx-attribute": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-8.0.0.tgz", - "integrity": "sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA==", - "dev": true, - "engines": { - "node": ">=14" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@svgr/babel-plugin-remove-jsx-empty-expression": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-8.0.0.tgz", - "integrity": "sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA==", - "dev": true, - "engines": { - "node": ">=14" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@svgr/babel-plugin-replace-jsx-attribute-value": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-8.0.0.tgz", - "integrity": "sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ==", - "dev": true, - "engines": { - "node": ">=14" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@svgr/babel-plugin-svg-dynamic-title": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-8.0.0.tgz", - "integrity": "sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og==", - "dev": true, - "engines": { - "node": ">=14" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@svgr/babel-plugin-svg-em-dimensions": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-8.0.0.tgz", - "integrity": "sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g==", - "dev": true, - "engines": { - "node": ">=14" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@svgr/babel-plugin-transform-react-native-svg": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-8.1.0.tgz", - "integrity": "sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q==", - "dev": true, - "engines": { - "node": ">=14" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@svgr/babel-plugin-transform-svg-component": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-8.0.0.tgz", - "integrity": "sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@svgr/babel-preset": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-8.1.0.tgz", - "integrity": "sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug==", - "dev": true, - "dependencies": { - "@svgr/babel-plugin-add-jsx-attribute": "8.0.0", - "@svgr/babel-plugin-remove-jsx-attribute": "8.0.0", - "@svgr/babel-plugin-remove-jsx-empty-expression": "8.0.0", - "@svgr/babel-plugin-replace-jsx-attribute-value": "8.0.0", - "@svgr/babel-plugin-svg-dynamic-title": "8.0.0", - "@svgr/babel-plugin-svg-em-dimensions": "8.0.0", - "@svgr/babel-plugin-transform-react-native-svg": "8.1.0", - "@svgr/babel-plugin-transform-svg-component": "8.0.0" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@svgr/core": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz", - "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", - "dev": true, - "dependencies": { - "@babel/core": "^7.21.3", - "@svgr/babel-preset": "8.1.0", - "camelcase": "^6.2.0", - "cosmiconfig": "^8.1.3", - "snake-case": "^3.0.4" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@svgr/hast-util-to-babel-ast": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-8.0.0.tgz", - "integrity": "sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q==", - "dev": true, - "dependencies": { - "@babel/types": "^7.21.3", - "entities": "^4.4.0" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@svgr/plugin-jsx": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-8.1.0.tgz", - "integrity": "sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA==", - "dev": true, - "dependencies": { - "@babel/core": "^7.21.3", - "@svgr/babel-preset": "8.1.0", - "@svgr/hast-util-to-babel-ast": "8.0.0", - "svg-parser": "^2.0.4" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - }, - "peerDependencies": { - "@svgr/core": "*" - } - }, - "node_modules/@swc/core": { - "version": "1.3.89", - "dev": true, - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "@swc/counter": "^0.1.1", - "@swc/types": "^0.1.5" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/swc" - }, - "optionalDependencies": { - "@swc/core-darwin-arm64": "1.3.89", - "@swc/core-darwin-x64": "1.3.89", - "@swc/core-linux-arm-gnueabihf": "1.3.89", - "@swc/core-linux-arm64-gnu": "1.3.89", - "@swc/core-linux-arm64-musl": "1.3.89", - "@swc/core-linux-x64-gnu": "1.3.89", - "@swc/core-linux-x64-musl": "1.3.89", - "@swc/core-win32-arm64-msvc": "1.3.89", - "@swc/core-win32-ia32-msvc": "1.3.89", - "@swc/core-win32-x64-msvc": "1.3.89" - }, - "peerDependencies": { - "@swc/helpers": "^0.5.0" - }, - "peerDependenciesMeta": { - "@swc/helpers": { - "optional": true - } - } - }, - "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.3.89", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-x64-musl": { - "version": "1.3.89", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/counter": { - "version": "0.1.1", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/@swc/types": { - "version": "0.1.5", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/@types/eslint": { - "version": "8.44.3", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "node_modules/@types/estree": { - "version": "1.0.2", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.13", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "20.8.7", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.7.tgz", - "integrity": "sha512-21TKHHh3eUHIi2MloeptJWALuCu5H7HQTdTrWIFReA8ad+aggoX+lRes3ex7/FtpC+sVUpFMQ+QTfYr74mruiQ==", - "dev": true, - "dependencies": { - "undici-types": "~5.25.1" - } - }, - "node_modules/@types/prop-types": { - "version": "15.7.7", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/react": { - "version": "18.2.22", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/prop-types": "*", - "@types/scheduler": "*", - "csstype": "^3.0.2" - } - }, - "node_modules/@types/react-dom": { - "version": "18.2.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/react": "*" - } - }, - "node_modules/@types/scheduler": { - "version": "0.16.4", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/semver": { - "version": "7.5.3", - "dev": true, - "license": "MIT" - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "6.7.3", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.7.3", - "@typescript-eslint/type-utils": "6.7.3", - "@typescript-eslint/utils": "6.7.3", - "@typescript-eslint/visitor-keys": "6.7.3", - "debug": "^4.3.4", - "graphemer": "^1.4.0", - "ignore": "^5.2.4", - "natural-compare": "^1.4.0", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", - "eslint": "^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "6.7.3", - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/scope-manager": "6.7.3", - "@typescript-eslint/types": "6.7.3", - "@typescript-eslint/typescript-estree": "6.7.3", - "@typescript-eslint/visitor-keys": "6.7.3", - "debug": "^4.3.4" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "6.7.3", - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "6.7.3", - "@typescript-eslint/visitor-keys": "6.7.3" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "6.7.3", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/typescript-estree": "6.7.3", - "@typescript-eslint/utils": "6.7.3", - "debug": "^4.3.4", - "ts-api-utils": "^1.0.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/types": { - "version": "6.7.3", - "license": "MIT", - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.7.3", - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/types": "6.7.3", - "@typescript-eslint/visitor-keys": "6.7.3", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "6.7.3", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@types/json-schema": "^7.0.12", - "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.7.3", - "@typescript-eslint/types": "6.7.3", - "@typescript-eslint/typescript-estree": "6.7.3", - "semver": "^7.5.4" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.7.3", - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "6.7.3", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@vitejs/plugin-react-swc": { - "version": "3.4.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@swc/core": "^1.3.85" - }, - "peerDependencies": { - "vite": "^4" - } - }, - "node_modules/acorn": { - "version": "8.10.0", - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/any-promise": { - "version": "1.3.0", - "dev": true, - "license": "MIT" - }, - "node_modules/anymatch": { - "version": "3.1.3", - "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/arg": { - "version": "5.0.2", - "dev": true, - "license": "MIT" - }, - "node_modules/argparse": { - "version": "2.0.1", - "license": "Python-2.0" - }, - "node_modules/array-union": { - "version": "2.1.0", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, - "node_modules/autoprefixer": { - "version": "10.4.16", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "browserslist": "^4.21.10", - "caniuse-lite": "^1.0.30001538", - "fraction.js": "^4.3.6", - "normalize-range": "^0.1.2", - "picocolors": "^1.0.0", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/axios": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.5.1.tgz", - "integrity": "sha512-Q28iYCWzNHjAm+yEAot5QaAMxhMghWLFVf7rRdwhUI+c2jix2DUXjAHXVi+s1ibs3mjPO/cCgbA++3BjD0vP/A==", - "dependencies": { - "follow-redirects": "^1.15.0", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, - "node_modules/axios-auth-refresh": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/axios-auth-refresh/-/axios-auth-refresh-3.3.6.tgz", - "integrity": "sha512-2CeBUce/SxIfFxow5/n8vApJ97yYF6qoV4gh1UrswT7aEOnlOdBLxxyhOI4IaxGs6BY0l8YujU2jlc4aCmK17Q==", - "peerDependencies": { - "axios": ">= 0.18 < 0.19.0 || >= 0.19.1" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "license": "MIT" - }, - "node_modules/binary-extensions": { - "version": "2.2.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.2", - "license": "MIT", - "dependencies": { - "fill-range": "^7.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.21.11", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "caniuse-lite": "^1.0.30001538", - "electron-to-chromium": "^1.4.526", - "node-releases": "^2.0.13", - "update-browserslist-db": "^1.0.13" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "license": "MIT", - "engines": { - "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", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001539", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/chalk": { - "version": "4.1.2", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chokidar": { - "version": "3.5.3", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/class-variance-authority": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.0.tgz", - "integrity": "sha512-jFI8IQw4hczaL4ALINxqLEXQbWcNjoSkloa4IaufXCJr6QawJyw7tuRysRsrE8w2p/4gGaxKIt/hX3qz/IbD1A==", - "dependencies": { - "clsx": "2.0.0" - }, - "funding": { - "url": "https://joebell.co.uk" - } - }, - "node_modules/client-only": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", - "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" - }, - "node_modules/clsx": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz", - "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==", - "engines": { - "node": ">=6" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "license": "MIT" - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/commander": { - "version": "4.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "license": "MIT" - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true - }, - "node_modules/cosmiconfig": { - "version": "8.3.6", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", - "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", - "dev": true, - "dependencies": { - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0", - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/d-fischer" - }, - "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/cross-spawn": { - "version": "7.0.3", - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/cssesc": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/csstype": { - "version": "3.1.2", - "dev": true, - "license": "MIT" - }, - "node_modules/debug": { - "version": "4.3.4", - "license": "MIT", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "license": "MIT" - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/didyoumean": { - "version": "1.2.2", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/dir-glob": { - "version": "3.0.1", - "license": "MIT", - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/dlv": { - "version": "1.1.3", - "dev": true, - "license": "MIT" - }, - "node_modules/doctrine": { - "version": "3.0.0", - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/dot-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", - "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", - "dev": true, - "dependencies": { - "no-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.4.528", - "dev": true, - "license": "ISC" - }, - "node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "dev": true, - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/esbuild": { - "version": "0.18.20", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/android-arm": "0.18.20", - "@esbuild/android-arm64": "0.18.20", - "@esbuild/android-x64": "0.18.20", - "@esbuild/darwin-arm64": "0.18.20", - "@esbuild/darwin-x64": "0.18.20", - "@esbuild/freebsd-arm64": "0.18.20", - "@esbuild/freebsd-x64": "0.18.20", - "@esbuild/linux-arm": "0.18.20", - "@esbuild/linux-arm64": "0.18.20", - "@esbuild/linux-ia32": "0.18.20", - "@esbuild/linux-loong64": "0.18.20", - "@esbuild/linux-mips64el": "0.18.20", - "@esbuild/linux-ppc64": "0.18.20", - "@esbuild/linux-riscv64": "0.18.20", - "@esbuild/linux-s390x": "0.18.20", - "@esbuild/linux-x64": "0.18.20", - "@esbuild/netbsd-x64": "0.18.20", - "@esbuild/openbsd-x64": "0.18.20", - "@esbuild/sunos-x64": "0.18.20", - "@esbuild/win32-arm64": "0.18.20", - "@esbuild/win32-ia32": "0.18.20", - "@esbuild/win32-x64": "0.18.20" - } - }, - "node_modules/escalade": { - "version": "3.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "8.50.0", - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.2", - "@eslint/js": "8.50.0", - "@humanwhocodes/config-array": "^0.11.11", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-plugin-react-hooks": { - "version": "4.6.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" - } - }, - "node_modules/eslint-plugin-react-refresh": { - "version": "0.4.3", - "dev": true, - "license": "MIT", - "peerDependencies": { - "eslint": ">=7" - } - }, - "node_modules/eslint-scope": { - "version": "7.2.2", - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/espree": { - "version": "9.6.1", - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.5.0", - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", - "dev": true - }, - "node_modules/esutils": { - "version": "2.0.3", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "license": "MIT" - }, - "node_modules/fast-glob": { - "version": "3.3.1", - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "license": "MIT" - }, - "node_modules/fastq": { - "version": "1.15.0", - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/file-entry-cache": { - "version": "6.0.1", - "license": "MIT", - "dependencies": { - "flat-cache": "^3.0.4" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/fill-range": { - "version": "7.0.1", - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "3.1.0", - "license": "MIT", - "dependencies": { - "flatted": "^3.2.7", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/flatted": { - "version": "3.2.9", - "license": "ISC" - }, - "node_modules/follow-redirects": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", - "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fraction.js": { - "version": "4.3.6", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - }, - "funding": { - "type": "patreon", - "url": "https://github.com/sponsors/rawify" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "license": "ISC" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.1", - "dev": true, - "license": "MIT" - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/glob": { - "version": "7.1.6", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "3.0.8", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/globals": { - "version": "13.22.0", - "license": "MIT", - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globby": { - "version": "11.1.0", - "license": "MIT", - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/graphemer": { - "version": "1.4.0", - "license": "MIT" - }, - "node_modules/has": { - "version": "1.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ignore": { - "version": "5.2.4", - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/import-fresh": { - "version": "3.3.0", - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "license": "ISC" - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-core-module": { - "version": "2.13.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "license": "ISC" - }, - "node_modules/jiti": { - "version": "1.20.0", - "dev": true, - "license": "MIT", - "bin": { - "jiti": "bin/jiti.js" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "dev": true, - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "license": "MIT" - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "license": "MIT" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/jwt-decode": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz", - "integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==" - }, - "node_modules/keyv": { - "version": "4.5.3", - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/lilconfig": { - "version": "2.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "dev": true, - "license": "MIT" - }, - "node_modules/locate-path": { - "version": "6.0.0", - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "license": "MIT" - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/lower-case": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", - "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", - "dev": true, - "dependencies": { - "tslib": "^2.0.3" - } - }, - "node_modules/lru-cache": { - "version": "6.0.0", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/merge2": { - "version": "1.4.1", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.5", - "license": "MIT", - "dependencies": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/ms": { - "version": "2.1.2", - "license": "MIT" - }, - "node_modules/mz": { - "version": "2.7.0", - "dev": true, - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, - "node_modules/nanoid": { - "version": "3.3.6", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "license": "MIT" - }, - "node_modules/no-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", - "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", - "dev": true, - "dependencies": { - "lower-case": "^2.0.2", - "tslib": "^2.0.3" - } - }, - "node_modules/node-releases": { - "version": "2.0.13", - "dev": true, - "license": "MIT" - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/normalize-range": { - "version": "0.1.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-hash": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/once": { - "version": "1.4.0", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/optionator": { - "version": "0.9.3", - "license": "MIT", - "dependencies": { - "@aashutoshrathi/word-wrap": "^1.2.3", - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "dev": true, - "license": "MIT" - }, - "node_modules/path-type": { - "version": "4.0.0", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/picocolors": { - "version": "1.0.0", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pify": { - "version": "2.3.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pirates": { - "version": "4.0.6", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-import": { - "version": "15.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.0.0", - "read-cache": "^1.0.0", - "resolve": "^1.1.7" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "postcss": "^8.0.0" - } - }, - "node_modules/postcss-js": { - "version": "4.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "camelcase-css": "^2.0.1" - }, - "engines": { - "node": "^12 || ^14 || >= 16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "postcss": "^8.4.21" - } - }, - "node_modules/postcss-load-config": { - "version": "4.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "lilconfig": "^2.0.5", - "yaml": "^2.1.1" - }, - "engines": { - "node": ">= 14" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "postcss": ">=8.0.9", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "postcss": { - "optional": true - }, - "ts-node": { - "optional": true - } - } - }, - "node_modules/postcss-nested": { - "version": "6.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^6.0.11" - }, - "engines": { - "node": ">=12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "postcss": "^8.2.14" - } - }, - "node_modules/postcss-selector-parser": { - "version": "6.0.13", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "dev": true, - "license": "MIT" - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prettier": { - "version": "3.0.3", - "dev": true, - "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/prettier-plugin-tailwindcss": { - "version": "0.5.4", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.21.3" - }, - "peerDependencies": { - "@ianvs/prettier-plugin-sort-imports": "*", - "@prettier/plugin-pug": "*", - "@shopify/prettier-plugin-liquid": "*", - "@shufo/prettier-plugin-blade": "*", - "@trivago/prettier-plugin-sort-imports": "*", - "prettier": "^3.0", - "prettier-plugin-astro": "*", - "prettier-plugin-css-order": "*", - "prettier-plugin-import-sort": "*", - "prettier-plugin-jsdoc": "*", - "prettier-plugin-organize-attributes": "*", - "prettier-plugin-organize-imports": "*", - "prettier-plugin-style-order": "*", - "prettier-plugin-svelte": "*" - }, - "peerDependenciesMeta": { - "@ianvs/prettier-plugin-sort-imports": { - "optional": true - }, - "@prettier/plugin-pug": { - "optional": true - }, - "@shopify/prettier-plugin-liquid": { - "optional": true - }, - "@shufo/prettier-plugin-blade": { - "optional": true - }, - "@trivago/prettier-plugin-sort-imports": { - "optional": true - }, - "prettier-plugin-astro": { - "optional": true - }, - "prettier-plugin-css-order": { - "optional": true - }, - "prettier-plugin-import-sort": { - "optional": true - }, - "prettier-plugin-jsdoc": { - "optional": true - }, - "prettier-plugin-marko": { - "optional": true - }, - "prettier-plugin-organize-attributes": { - "optional": true - }, - "prettier-plugin-organize-imports": { - "optional": true - }, - "prettier-plugin-style-order": { - "optional": true - }, - "prettier-plugin-svelte": { - "optional": true - }, - "prettier-plugin-twig-melody": { - "optional": true - } - } - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" - }, - "node_modules/punycode": { - "version": "2.3.0", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/react": { - "version": "18.2.0", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "18.2.0", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.0" - }, - "peerDependencies": { - "react": "^18.2.0" - } - }, - "node_modules/react-router": { - "version": "6.17.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.17.0.tgz", - "integrity": "sha512-YJR3OTJzi3zhqeJYADHANCGPUu9J+6fT5GLv82UWRGSxu6oJYCKVmxUcaBQuGm9udpWmPsvpme/CdHumqgsoaA==", - "dependencies": { - "@remix-run/router": "1.10.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "react": ">=16.8" - } - }, - "node_modules/react-router-dom": { - "version": "6.17.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.17.0.tgz", - "integrity": "sha512-qWHkkbXQX+6li0COUUPKAUkxjNNqPJuiBd27dVwQGDNsuFBdMbrS6UZ0CLYc4CsbdLYTckn4oB4tGDuPZpPhaQ==", - "dependencies": { - "@remix-run/router": "1.10.0", - "react-router": "6.17.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "react": ">=16.8", - "react-dom": ">=16.8" - } - }, - "node_modules/read-cache": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "pify": "^2.3.0" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "dev": true, - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/regenerator-runtime": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", - "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" - }, - "node_modules/resolve": { - "version": "1.22.6", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/reusify": { - "version": "1.0.4", - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rimraf": { - "version": "3.0.2", - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/glob": { - "version": "7.2.3", - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rollup": { - "version": "3.29.3", - "dev": true, - "license": "MIT", - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=14.18.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/scheduler": { - "version": "0.23.0", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - } - }, - "node_modules/semver": { - "version": "7.5.4", - "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/slash": { - "version": "3.0.0", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/snake-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", - "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", - "dev": true, - "dependencies": { - "dot-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, - "node_modules/source-map-js": { - "version": "1.0.2", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/sucrase": { - "version": "3.34.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.2", - "commander": "^4.0.0", - "glob": "7.1.6", - "lines-and-columns": "^1.1.6", - "mz": "^2.7.0", - "pirates": "^4.0.1", - "ts-interface-checker": "^0.1.9" - }, - "bin": { - "sucrase": "bin/sucrase", - "sucrase-node": "bin/sucrase-node" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/svg-parser": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", - "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==", - "dev": true - }, - "node_modules/swr": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/swr/-/swr-2.2.4.tgz", - "integrity": "sha512-njiZ/4RiIhoOlAaLYDqwz5qH/KZXVilRLvomrx83HjzCWTfa+InyfAjv05PSFxnmLzZkNO9ZfvgoqzAaEI4sGQ==", - "dependencies": { - "client-only": "^0.0.1", - "use-sync-external-store": "^1.2.0" - }, - "peerDependencies": { - "react": "^16.11.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/tailwind-merge": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.1.0.tgz", - "integrity": "sha512-l11VvI4nSwW7MtLSLYT4ldidDEUwQAMWuSHk7l4zcXZDgnCRa0V3OdCwFfM7DCzakVXMNRwAeje9maFFXT71dQ==", - "dependencies": { - "@babel/runtime": "^7.23.5" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/dcastil" - } - }, - "node_modules/tailwindcss": { - "version": "3.3.3", - "dev": true, - "license": "MIT", - "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "arg": "^5.0.2", - "chokidar": "^3.5.3", - "didyoumean": "^1.2.2", - "dlv": "^1.1.3", - "fast-glob": "^3.2.12", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "jiti": "^1.18.2", - "lilconfig": "^2.1.0", - "micromatch": "^4.0.5", - "normalize-path": "^3.0.0", - "object-hash": "^3.0.0", - "picocolors": "^1.0.0", - "postcss": "^8.4.23", - "postcss-import": "^15.1.0", - "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.1", - "postcss-nested": "^6.0.1", - "postcss-selector-parser": "^6.0.11", - "resolve": "^1.22.2", - "sucrase": "^3.32.0" - }, - "bin": { - "tailwind": "lib/cli.js", - "tailwindcss": "lib/cli.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/text-table": { - "version": "0.2.0", - "license": "MIT" - }, - "node_modules/thenify": { - "version": "3.3.1", - "dev": true, - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0" - } - }, - "node_modules/thenify-all": { - "version": "1.6.0", - "dev": true, - "license": "MIT", - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/ts-api-utils": { - "version": "1.0.3", - "license": "MIT", - "engines": { - "node": ">=16.13.0" - }, - "peerDependencies": { - "typescript": ">=4.2.0" - } - }, - "node_modules/ts-interface-checker": { - "version": "0.1.13", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", - "dev": true - }, - "node_modules/type-check": { - "version": "0.4.0", - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/type-fest": { - "version": "0.20.2", - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/typescript": { - "version": "5.2.2", - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici-types": { - "version": "5.25.3", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.25.3.tgz", - "integrity": "sha512-Ga1jfYwRn7+cP9v8auvEXN1rX3sWqlayd4HP7OKk4mZWylEmu3KzXDUGrQUN6Ol7qo1gPvB2e5gX6udnyEPgdA==", - "dev": true - }, - "node_modules/update-browserslist-db": { - "version": "1.0.13", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/use-sync-external-store": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", - "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/usehooks-ts": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/usehooks-ts/-/usehooks-ts-2.9.1.tgz", - "integrity": "sha512-2FAuSIGHlY+apM9FVlj8/oNhd+1y+Uwv5QNkMQz1oSfdHk4PXo1qoCw9I5M7j0vpH8CSWFJwXbVPeYDjLCx9PA==", - "engines": { - "node": ">=16.15.0", - "npm": ">=8" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "dev": true, - "license": "MIT" - }, - "node_modules/vite": { - "version": "4.4.9", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.18.10", - "postcss": "^8.4.27", - "rollup": "^3.27.1" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - }, - "peerDependencies": { - "@types/node": ">= 14", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } - } - }, - "node_modules/vite-plugin-svgr": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/vite-plugin-svgr/-/vite-plugin-svgr-4.1.0.tgz", - "integrity": "sha512-v7Qic+FWmCChgQNGSI4V8X63OEYsdUoLt66iqIcHozq9bfK/Dwmr0V+LBy1NE8CE98Y8HouEBJ+pto4AMfN5xw==", - "dev": true, - "dependencies": { - "@rollup/pluginutils": "^5.0.4", - "@svgr/core": "^8.1.0", - "@svgr/plugin-jsx": "^8.1.0" - }, - "peerDependencies": { - "vite": "^2.6.0 || 3 || 4" - } - }, - "node_modules/which": { - "version": "2.0.2", - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "license": "ISC" - }, - "node_modules/yallist": { - "version": "4.0.0", - "license": "ISC" - }, - "node_modules/yaml": { - "version": "2.3.2", - "dev": true, - "license": "ISC", - "engines": { - "node": ">= 14" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - } -} diff --git a/package.json b/package.json index f8a53625..7f67a8ec 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@react-oauth/google": "^0.11.1", + "@tanstack/react-router": "beta", "@typescript-eslint/parser": "^6.7.3", "axios": "^1.5.1", "axios-auth-refresh": "^3.3.6", @@ -20,12 +21,12 @@ "jwt-decode": "^3.1.2", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-router-dom": "^6.17.0", "swr": "^2.2.4", "tailwind-merge": "^2.0.0", "usehooks-ts": "^2.9.1" }, "devDependencies": { + "@tanstack/router-devtools": "beta", "@types/eslint": "^8.44.3", "@types/node": "^20.8.7", "@types/react": "^18.2.15", diff --git a/src/App.tsx b/src/App.tsx index f84b4023..d962b572 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,52 +1,111 @@ import { GoogleOAuthProvider } from '@react-oauth/google' -import { RouterProvider, createBrowserRouter, redirect } from 'react-router-dom' -import { DashboardPage, LeaderboardDiscipline, ContestDiscipline } from './pages' import './App.tw.css' -import { redirectToOngoingContest, redirectToDefaultDiscipline } from './loaders' -import { DevResetSession, DisciplinesTabsLayout, Layout } from './components' +import { RootRoute, Route, Router, RouterProvider } from '@tanstack/react-router' import { CubeProvider } from './integrations/cube' import { ReconstructorProvider } from './integrations/reconstructor' +import { Layout, DisciplinesTabsLayout, DevResetSession } from './components' +import { DashboardPage, ContestDiscipline, LeaderboardDiscipline } from './pages' +import { TanStackRouterDevtools } from '@tanstack/router-devtools' +import { DEFAULT_DISCIPLINE, isDiscipline } from './types' +import { getOngoingContestNumber } from './api/contests' -const router = createBrowserRouter([ - { - path: '/', - element: , - children: [ - { - path: '/', - element: , - }, - { path: '*', loader: () => redirect('/') }, - { - path: 'contest', - loader: redirectToOngoingContest, - }, - { - path: 'contest/:contestNumber', - loader: redirectToDefaultDiscipline, - element: , - children: [ - { - path: ':discipline', - element: , - }, - ], - }, - { - path: 'leaderboard', - loader: redirectToDefaultDiscipline, - element: , - children: [ - { - path: ':discipline', - element: , - }, - ], - }, - { path: 'dev/reset-session', element: }, - ], +const rootRoute = new RootRoute({ + component: () => ( + <> + + {import.meta.env.MODE === 'development' && } + + ), +}) +const indexRoute = new Route({ getParentRoute: () => rootRoute, path: '/', component: DashboardPage }) + +const leaderboardRoute = new Route({ + getParentRoute: () => rootRoute, + path: '/leaderboard', +}) +const leaderboardIndexRoute = new Route({ + getParentRoute: () => leaderboardRoute, + path: '/', + beforeLoad: ({ navigate }) => { + navigate({ to: '$discipline', params: { discipline: DEFAULT_DISCIPLINE }, replace: true }) + }, +}) +export const leaderboardDisciplineRoute = new Route({ + getParentRoute: () => leaderboardRoute, + path: '$discipline', + loader: ({ params, navigate }) => { + if (!isDiscipline(params.discipline)) { + navigate({ to: '../', replace: true }) + } + }, + component: () => ( + + + + ), +}) + +const allContestsRoute = new Route({ getParentRoute: () => rootRoute, path: '/contest' }) +const allContestsIndexRoute = new Route({ + getParentRoute: () => allContestsRoute, + path: '/', + beforeLoad: async ({ navigate }) => { + navigate({ to: '$contestNumber', params: { contestNumber: await getOngoingContestNumber() }, replace: true }) + }, +}) +const contestRoute = new Route({ + getParentRoute: () => allContestsRoute, + path: '$contestNumber', +}) +const contestIndexRoute = new Route({ + getParentRoute: () => contestRoute, + path: '/', + beforeLoad: ({ navigate }) => { + navigate({ to: '$discipline', params: { discipline: DEFAULT_DISCIPLINE }, replace: true }) }, +}) + +export const contestDisciplineRoute = new Route({ + getParentRoute: () => contestRoute, + path: '$discipline', + loader: ({ params, navigate }) => { + const contestNumber = Number(params.contestNumber) + if (isNaN(contestNumber)) { + navigate({ to: '../../', replace: true }) + } + if (!isDiscipline(params.discipline)) { + navigate({ to: '../', replace: true }) + } + }, + component: () => ( + + + + ), +}) + +const devResetSessionRoute = new Route({ + getParentRoute: () => rootRoute, + path: 'dev/reset-session', + component: DevResetSession, +}) + +const routeTree = rootRoute.addChildren([ + indexRoute, + leaderboardRoute.addChildren([leaderboardIndexRoute, leaderboardDisciplineRoute]), + allContestsRoute.addChildren([ + allContestsIndexRoute, + contestRoute.addChildren([contestDisciplineRoute, contestIndexRoute]), + ]), + devResetSessionRoute, ]) +const router = new Router({ routeTree }) + +declare module '@tanstack/react-router' { + interface Register { + router: typeof router + } +} export function App() { return ( diff --git a/src/api/contests/useContestResults.ts b/src/api/contests/useContestResults.ts index 39f93b21..047412dc 100644 --- a/src/api/contests/useContestResults.ts +++ b/src/api/contests/useContestResults.ts @@ -4,12 +4,12 @@ import { axiosClient } from '../axios' export type ContestResultsResponse = Array<{ id: number - avgMs: number | null // TODO fix to camelCase + avgMs: number | null discipline: { name: Discipline } user: { username: string } solveSet: Array<{ id: number - timeMs: number | null // TODO fix to camelCase + timeMs: number | null dnf: boolean scramble: Pick state: 'submitted' | 'changed_to_extra' diff --git a/src/components/DevResetSession.tsx b/src/components/DevResetSession.tsx index 5546a8ae..e3d510f0 100644 --- a/src/components/DevResetSession.tsx +++ b/src/components/DevResetSession.tsx @@ -1,12 +1,12 @@ import { axiosClient } from '@/api/axios' -import { useNavigate } from 'react-router-dom' +import { useNavigate } from '@tanstack/react-router' export function DevResetSession() { const navigate = useNavigate() const resetSession = async () => { try { await axiosClient.delete('/contests/round-session/') - navigate('/contest') + navigate({ to: '/contest' }) } catch (err) { alert("either you don't have any results to reset or something went wrong") } diff --git a/src/components/DisciplinesTabsLayout.tsx b/src/components/DisciplinesTabsLayout.tsx index f7c3be15..7c5ad49a 100644 --- a/src/components/DisciplinesTabsLayout.tsx +++ b/src/components/DisciplinesTabsLayout.tsx @@ -1,13 +1,13 @@ -import { Outlet } from 'react-router-dom' import CubeIcon from '@/assets/3by3.svg?react' +import { ReactNode } from 'react' -export function DisciplinesTabsLayout() { +export function DisciplinesTabsLayout({ children }: { children: ReactNode }) { return ( <>

    - + {children} ) } diff --git a/src/components/layout/Layout.tsx b/src/components/layout/Layout.tsx index 48c451bd..7be84756 100644 --- a/src/components/layout/Layout.tsx +++ b/src/components/layout/Layout.tsx @@ -1,6 +1,6 @@ -import { Outlet } from 'react-router-dom' import { LoginSection, NavBar } from './components' import { PickUsernameModal } from '..' +import { Outlet } from '@tanstack/react-router' export function Layout() { return ( diff --git a/src/components/layout/components/Navbar.tsx b/src/components/layout/components/Navbar.tsx index f2915cec..f61b328d 100644 --- a/src/components/layout/components/Navbar.tsx +++ b/src/components/layout/components/Navbar.tsx @@ -1,22 +1,22 @@ import { useOngoingContestNumber } from '@/api/contests' import { cn } from '@/utils' +import { Link, useParams } from '@tanstack/react-router' import { ButtonHTMLAttributes, useMemo, useState } from 'react' -import { useParams, NavLink } from 'react-router-dom' export function NavBar() { const { data: ongoingContestNumber } = useOngoingContestNumber() - const params = useParams() + const params = useParams({ strict: false }) const openedContestNumber = params?.contestNumber ? Number(params?.contestNumber) : null const [isMobileNavVisible, setIsMovileNavVisible] = useState(false) const links = useMemo(() => { const list = [ { text: 'Dashboard', to: '/' }, - { text: 'Leaderboard', to: `/leaderboard` }, + { text: 'Leaderboard', to: '/leaderboard' }, { text: 'Ongoing contest', to: `/contest/${ongoingContestNumber}` }, ] - if (openedContestNumber && openedContestNumber !== ongoingContestNumber) { + if (ongoingContestNumber && openedContestNumber && openedContestNumber !== ongoingContestNumber) { list.push({ text: `Contest ${openedContestNumber}`, to: `/contest/${openedContestNumber}` }) } @@ -36,18 +36,15 @@ export function NavBar() { > {links.map(({ text, to }) => (
  1. - - cn( - isActive ? 'pointer-events-none border-primary text-white' : 'text-white/50 md:border-transparent', - 'block px-5 py-3 md:flex md:h-full md:items-center md:border-t-[3px] md:px-0 md:py-0 lg:text-xl', - ) - } + className='block px-5 py-3 md:flex md:h-full md:items-center md:border-t-[3px] md:px-0 md:py-0 lg:text-xl' + activeProps={{ className: 'pointer-events-none border-primary text-white' }} + inactiveProps={{ className: 'text-white/50 md:border-transparent' }} onClick={() => setIsMovileNavVisible(false)} > {text} - +
  2. ))} diff --git a/src/constants.ts b/src/constants.ts deleted file mode 100644 index 5ee65549..00000000 --- a/src/constants.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { Discipline } from './types' - -export const DEFAULT_DISCIPLINE: Discipline = '3by3' diff --git a/src/integrations/cube/Cube.tsx b/src/integrations/cube/Cube.tsx index 4606be3e..f447db6e 100644 --- a/src/integrations/cube/Cube.tsx +++ b/src/integrations/cube/Cube.tsx @@ -2,7 +2,7 @@ import { RefObject, useEffect, useState } from 'react' export type CubeSolveResult = | { reconstruction: string; timeMs: number; dnf: false } - | { reconstruction: null; timeMs: null; dnf: true } // TODO fix to camelCase + | { reconstruction: null; timeMs: null; dnf: true } export type CubeSolveFinishCallback = (result: CubeSolveResult) => void export type CubeTimeStartCallback = () => void diff --git a/src/loaders/index.ts b/src/loaders/index.ts deleted file mode 100644 index 75323ece..00000000 --- a/src/loaders/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './redirectToOngoingContest' -export * from './redirectToDefaultDiscipline' diff --git a/src/loaders/redirectToDefaultDiscipline.ts b/src/loaders/redirectToDefaultDiscipline.ts deleted file mode 100644 index 71889df1..00000000 --- a/src/loaders/redirectToDefaultDiscipline.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { DEFAULT_DISCIPLINE } from '@/constants' -import { isDiscipline } from '@/types' -import { LoaderFunctionArgs, redirect } from 'react-router-dom' - -export async function redirectToDefaultDiscipline({ params }: LoaderFunctionArgs) { - const discipline = params.discipline - - if (!discipline || !isDiscipline(discipline)) { - return redirect(DEFAULT_DISCIPLINE) - } - return null -} diff --git a/src/loaders/redirectToOngoingContest.ts b/src/loaders/redirectToOngoingContest.ts deleted file mode 100644 index 037e4ae5..00000000 --- a/src/loaders/redirectToOngoingContest.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { getOngoingContestNumber } from '@/api/contests' -import { redirect } from 'react-router-dom' - -export async function redirectToOngoingContest() { - const ongoing = await getOngoingContestNumber() - return redirect(String(ongoing)) -} diff --git a/src/pages/contest/ContestDiscipline.tsx b/src/pages/contest/ContestDiscipline.tsx index 24c014a3..8d35e57c 100644 --- a/src/pages/contest/ContestDiscipline.tsx +++ b/src/pages/contest/ContestDiscipline.tsx @@ -1,22 +1,18 @@ import { ContestResultsResponse, useContestResults } from '@/api/contests' -import { useCallback, useEffect } from 'react' import { Discipline } from '@/types' -import { useNavigate, useSearchParams } from 'react-router-dom' -import { useRequiredParams } from '@/utils' import { PublishedSession } from './components/PublishedSession' import { SolveContest } from './components/SolveContest' import { InfoBox } from '@/components' import { useUser } from '@/api/accounts' -import { useReconstructor } from '@/integrations/reconstructor' +import { contestDisciplineRoute } from '@/App' export function ContestDiscipline() { const { userData } = useUser() - const routeParams = useRequiredParams<{ contestNumber: string; discipline: string }>() + const routeParams = contestDisciplineRoute.useParams() const contestNumber = Number(routeParams.contestNumber) const disciplineName = routeParams.discipline as Discipline // TODO add type guard - useReconstructorFromSearchParam() const { data: sessions, error, isLoading } = useContestResults(contestNumber, disciplineName) if (isLoading) { @@ -49,32 +45,3 @@ export function ContestDiscipline() { ) } - -function useReconstructorFromSearchParam() { - const { showReconstruction, closeReconstruction } = useReconstructor() - const [searchParams, setSearchParams] = useSearchParams() - - const deleteParam = useCallback( - () => - setSearchParams((params) => { - params.delete('solveId') - return params - }), - [setSearchParams], - ) - - useEffect(() => { - const openedSolveId = Number(searchParams.get('solveId')) - if (!openedSolveId) { - closeReconstruction() - return - } - showReconstruction(openedSolveId, deleteParam) - }, [searchParams, deleteParam, showReconstruction, closeReconstruction]) -} - -export function useNavigateToSolve() { - const navigate = useNavigate() - const navigateToSolve = (solveId: number) => navigate(`?solveId=${solveId}`) - return { navigateToSolve } -} diff --git a/src/pages/contest/components/PublishedSession.tsx b/src/pages/contest/components/PublishedSession.tsx index c1db0205..db96e9e8 100644 --- a/src/pages/contest/components/PublishedSession.tsx +++ b/src/pages/contest/components/PublishedSession.tsx @@ -2,7 +2,7 @@ import { ReconstructTimeButton, ResultCard } from '@/components' import { ContestResultsResponse } from '@/api/contests' import { cn, formatTimeResult } from '@/utils' import { useMemo } from 'react' -import { useNavigateToSolve } from '../ContestDiscipline' +import { useReconstructor } from '@/integrations/reconstructor' type PublishedSessionProps = ContestResultsResponse[number] export function PublishedSession({ @@ -12,7 +12,7 @@ export function PublishedSession({ isOwnSession, placeNumber, }: PublishedSessionProps & { isOwnSession?: boolean; placeNumber: number }) { - const { navigateToSolve } = useNavigateToSolve() + const { showReconstruction } = useReconstructor() const submittedSolves = useMemo(() => solveSet.filter(({ state }) => state === 'submitted'), [solveSet]) const { bestId, worstId } = useMemo(() => getBestAndWorstIds(submittedSolves), [submittedSolves]) @@ -43,7 +43,7 @@ export function PublishedSession({ 'w-[70px] md:w-[80px]', )} title={isExtra ? `Extra ${position[1]}` : undefined} - onClick={() => navigateToSolve(id)} + onClick={() => showReconstruction(id)} key={id} timeMs={timeMs} /> diff --git a/src/pages/contest/components/SolveContest/components/CurrentSolve.tsx b/src/pages/contest/components/SolveContest/components/CurrentSolve.tsx index 77649ef0..9784f5f2 100644 --- a/src/pages/contest/components/SolveContest/components/CurrentSolve.tsx +++ b/src/pages/contest/components/SolveContest/components/CurrentSolve.tsx @@ -1,9 +1,9 @@ import { ReconstructTimeButton } from '@/components' import { SolveContestStateResponse } from '@/api/contests' import { cn } from '@/utils' -import { useNavigateToSolve } from '@/pages/contest' import { useCube } from '@/integrations/cube' import { CubeSolveResult } from '@/integrations/cube' +import { useReconstructor } from '@/integrations/reconstructor' type CurrentSolveProps = SolveContestStateResponse['currentSolve'] & { className?: string @@ -21,7 +21,7 @@ export function CurrentSolve({ className, }: CurrentSolveProps) { const { initSolve } = useCube() - const { navigateToSolve } = useNavigateToSolve() + const { showReconstruction } = useReconstructor() const isFinished = !!solve const isSuccessful = isFinished && !solve.dnf @@ -39,7 +39,7 @@ export function CurrentSolve({ {!isFinished ? ( ??:??.?? ) : isSuccessful ? ( - navigateToSolve(solve.id)} timeMs={solve.timeMs} /> + showReconstruction(solve.id)} timeMs={solve.timeMs} /> ) : ( DNF )} diff --git a/src/pages/contest/components/SolveContest/components/SubmittedSolve.tsx b/src/pages/contest/components/SolveContest/components/SubmittedSolve.tsx index 36f7ca13..c07820f2 100644 --- a/src/pages/contest/components/SolveContest/components/SubmittedSolve.tsx +++ b/src/pages/contest/components/SolveContest/components/SubmittedSolve.tsx @@ -1,13 +1,13 @@ import { SolveContestStateResponse } from '@/api/contests' import { ReconstructTimeButton } from '@/components' -import { useNavigateToSolve } from '@/pages/contest' +import { useReconstructor } from '@/integrations/reconstructor' import { cn } from '@/utils' type SubmittedSolveProps = SolveContestStateResponse['submittedSolves'][number] & { className?: string } export function SubmittedSolve({ className, timeMs, scramble, id }: SubmittedSolveProps) { - const { navigateToSolve } = useNavigateToSolve() + const { showReconstruction } = useReconstructor() const isSuccessfull = timeMs !== null return ( @@ -20,7 +20,7 @@ export function SubmittedSolve({ className, timeMs, scramble, id }: SubmittedSol {scramble.position}. {isSuccessfull ? ( - navigateToSolve(id)} /> + showReconstruction(id)} /> ) : ( DNF )} diff --git a/src/pages/dashboard/components/BestSolves.tsx b/src/pages/dashboard/components/BestSolves.tsx index d7b784dd..38ff2eef 100644 --- a/src/pages/dashboard/components/BestSolves.tsx +++ b/src/pages/dashboard/components/BestSolves.tsx @@ -1,8 +1,8 @@ import { ReconstructTimeButton } from '@/components' -import { Link } from 'react-router-dom' import CubeIcon from '@/assets/3by3.svg?react' import { DashboardResponse } from '@/api/contests' import { useReconstructor } from '@/integrations/reconstructor' +import { Link } from '@tanstack/react-router' type BestSolvesProps = { bestSolves?: DashboardResponse['bestSolves'] } export function BestSolves({ bestSolves }: BestSolvesProps) { @@ -24,7 +24,11 @@ export function BestSolves({ bestSolves }: BestSolvesProps) { {username} showReconstruction(id)} /> - + leaderboard diff --git a/src/pages/dashboard/components/ContestsList.tsx b/src/pages/dashboard/components/ContestsList.tsx index 535ae545..5c3ab7e6 100644 --- a/src/pages/dashboard/components/ContestsList.tsx +++ b/src/pages/dashboard/components/ContestsList.tsx @@ -1,7 +1,7 @@ import { DashboardResponse } from '@/api/contests' -import { DEFAULT_DISCIPLINE } from '@/constants' +import { DEFAULT_DISCIPLINE } from '@/types' import { cn } from '@/utils' -import { Link } from 'react-router-dom' +import { Link } from '@tanstack/react-router' type ContestsListProps = { contests?: DashboardResponse['contests'] } export function ContestsList({ contests }: ContestsListProps) { @@ -23,7 +23,8 @@ export function ContestLink({ number, ongoing }: ContestLinkProps) { return ( Contest {number} {ongoing ? '(ongoing)' : null} diff --git a/src/pages/leaderboard/LeaderboardDiscipline.tsx b/src/pages/leaderboard/LeaderboardDiscipline.tsx index 85bd1d03..16c163d5 100644 --- a/src/pages/leaderboard/LeaderboardDiscipline.tsx +++ b/src/pages/leaderboard/LeaderboardDiscipline.tsx @@ -1,15 +1,15 @@ import { ReconstructTimeButton, ResultCard } from '@/components' import { Discipline } from '@/types' -import { useRequiredParams } from '@/utils' -import { Link } from 'react-router-dom' import { useLeaderboard, LeaderboardResponse } from '@/api/contests' import { useUser } from '@/api/accounts' import { useReconstructor } from '@/integrations/reconstructor' +import { leaderboardDisciplineRoute } from '@/App' +import { Link } from '@tanstack/react-router' export function LeaderboardDiscipline() { const { userData } = useUser() - const routeParams = useRequiredParams<{ discipline: string }>() - const discipline = routeParams.discipline as Discipline // TODO add type guard + const params = leaderboardDisciplineRoute.useParams() + const discipline = params.discipline as Discipline // TODO add type guard const { data: results } = useLeaderboard(discipline) @@ -56,7 +56,11 @@ function LeaderboardResult({
    {dateString} - + contest
    diff --git a/src/types.ts b/src/types.ts index 82dc7cda..42843eb9 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,7 +1,11 @@ export type Scramble = { scramble: string; position: string; extra: boolean; id: number } export type Discipline = (typeof DISCIPLINES)[number] +export const DEFAULT_DISCIPLINE: Discipline = '3by3' const DISCIPLINES = ['3by3'] as const export function isDiscipline(str: string): str is Discipline { return DISCIPLINES.includes(str as Discipline) } +export function castDiscipline(str: string): Discipline { + return isDiscipline(str) ? str : DEFAULT_DISCIPLINE +} diff --git a/src/utils/index.ts b/src/utils/index.ts index 462d518e..253497ec 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,5 +1,4 @@ export * from './formatTimeResult' -export * from './useRequiredParams' export * from './useConditionalBeforeUnload' export * from './cn' export * from './isTouchDevice' diff --git a/src/utils/useRequiredParams.ts b/src/utils/useRequiredParams.ts deleted file mode 100644 index 9747ab13..00000000 --- a/src/utils/useRequiredParams.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { useParams } from 'react-router-dom' - -export function useRequiredParams>() { - return useParams() as T -} From f60aee596f91df5fc037126140fd9a2661c89375 Mon Sep 17 00:00:00 2001 From: bohdancho Date: Sat, 23 Dec 2023 16:55:42 +0100 Subject: [PATCH 02/27] move router to a separate file --- src/App.tsx | 106 +----------------- src/pages/contest/ContestDiscipline.tsx | 2 +- .../leaderboard/LeaderboardDiscipline.tsx | 2 +- src/router.tsx | 104 +++++++++++++++++ 4 files changed, 108 insertions(+), 106 deletions(-) create mode 100644 src/router.tsx diff --git a/src/App.tsx b/src/App.tsx index d962b572..854706de 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,111 +1,9 @@ import { GoogleOAuthProvider } from '@react-oauth/google' import './App.tw.css' -import { RootRoute, Route, Router, RouterProvider } from '@tanstack/react-router' +import { RouterProvider } from '@tanstack/react-router' import { CubeProvider } from './integrations/cube' import { ReconstructorProvider } from './integrations/reconstructor' -import { Layout, DisciplinesTabsLayout, DevResetSession } from './components' -import { DashboardPage, ContestDiscipline, LeaderboardDiscipline } from './pages' -import { TanStackRouterDevtools } from '@tanstack/router-devtools' -import { DEFAULT_DISCIPLINE, isDiscipline } from './types' -import { getOngoingContestNumber } from './api/contests' - -const rootRoute = new RootRoute({ - component: () => ( - <> - - {import.meta.env.MODE === 'development' && } - - ), -}) -const indexRoute = new Route({ getParentRoute: () => rootRoute, path: '/', component: DashboardPage }) - -const leaderboardRoute = new Route({ - getParentRoute: () => rootRoute, - path: '/leaderboard', -}) -const leaderboardIndexRoute = new Route({ - getParentRoute: () => leaderboardRoute, - path: '/', - beforeLoad: ({ navigate }) => { - navigate({ to: '$discipline', params: { discipline: DEFAULT_DISCIPLINE }, replace: true }) - }, -}) -export const leaderboardDisciplineRoute = new Route({ - getParentRoute: () => leaderboardRoute, - path: '$discipline', - loader: ({ params, navigate }) => { - if (!isDiscipline(params.discipline)) { - navigate({ to: '../', replace: true }) - } - }, - component: () => ( - - - - ), -}) - -const allContestsRoute = new Route({ getParentRoute: () => rootRoute, path: '/contest' }) -const allContestsIndexRoute = new Route({ - getParentRoute: () => allContestsRoute, - path: '/', - beforeLoad: async ({ navigate }) => { - navigate({ to: '$contestNumber', params: { contestNumber: await getOngoingContestNumber() }, replace: true }) - }, -}) -const contestRoute = new Route({ - getParentRoute: () => allContestsRoute, - path: '$contestNumber', -}) -const contestIndexRoute = new Route({ - getParentRoute: () => contestRoute, - path: '/', - beforeLoad: ({ navigate }) => { - navigate({ to: '$discipline', params: { discipline: DEFAULT_DISCIPLINE }, replace: true }) - }, -}) - -export const contestDisciplineRoute = new Route({ - getParentRoute: () => contestRoute, - path: '$discipline', - loader: ({ params, navigate }) => { - const contestNumber = Number(params.contestNumber) - if (isNaN(contestNumber)) { - navigate({ to: '../../', replace: true }) - } - if (!isDiscipline(params.discipline)) { - navigate({ to: '../', replace: true }) - } - }, - component: () => ( - - - - ), -}) - -const devResetSessionRoute = new Route({ - getParentRoute: () => rootRoute, - path: 'dev/reset-session', - component: DevResetSession, -}) - -const routeTree = rootRoute.addChildren([ - indexRoute, - leaderboardRoute.addChildren([leaderboardIndexRoute, leaderboardDisciplineRoute]), - allContestsRoute.addChildren([ - allContestsIndexRoute, - contestRoute.addChildren([contestDisciplineRoute, contestIndexRoute]), - ]), - devResetSessionRoute, -]) -const router = new Router({ routeTree }) - -declare module '@tanstack/react-router' { - interface Register { - router: typeof router - } -} +import { router } from './router' export function App() { return ( diff --git a/src/pages/contest/ContestDiscipline.tsx b/src/pages/contest/ContestDiscipline.tsx index 8d35e57c..6a84bb2e 100644 --- a/src/pages/contest/ContestDiscipline.tsx +++ b/src/pages/contest/ContestDiscipline.tsx @@ -4,7 +4,7 @@ import { PublishedSession } from './components/PublishedSession' import { SolveContest } from './components/SolveContest' import { InfoBox } from '@/components' import { useUser } from '@/api/accounts' -import { contestDisciplineRoute } from '@/App' +import { contestDisciplineRoute } from '@/router' export function ContestDiscipline() { const { userData } = useUser() diff --git a/src/pages/leaderboard/LeaderboardDiscipline.tsx b/src/pages/leaderboard/LeaderboardDiscipline.tsx index 16c163d5..85bef760 100644 --- a/src/pages/leaderboard/LeaderboardDiscipline.tsx +++ b/src/pages/leaderboard/LeaderboardDiscipline.tsx @@ -3,8 +3,8 @@ import { Discipline } from '@/types' import { useLeaderboard, LeaderboardResponse } from '@/api/contests' import { useUser } from '@/api/accounts' import { useReconstructor } from '@/integrations/reconstructor' -import { leaderboardDisciplineRoute } from '@/App' import { Link } from '@tanstack/react-router' +import { leaderboardDisciplineRoute } from '@/router' export function LeaderboardDiscipline() { const { userData } = useUser() diff --git a/src/router.tsx b/src/router.tsx new file mode 100644 index 00000000..51f287d8 --- /dev/null +++ b/src/router.tsx @@ -0,0 +1,104 @@ +import { Layout, DisciplinesTabsLayout, DevResetSession } from './components' +import { DashboardPage, ContestDiscipline, LeaderboardDiscipline } from './pages' +import { TanStackRouterDevtools } from '@tanstack/router-devtools' +import { DEFAULT_DISCIPLINE, isDiscipline } from './types' +import { getOngoingContestNumber } from './api/contests' +import { RootRoute, Route, Router } from '@tanstack/react-router' + +const rootRoute = new RootRoute({ + component: () => ( + <> + + {import.meta.env.MODE === 'development' && } + + ), +}) +const indexRoute = new Route({ getParentRoute: () => rootRoute, path: '/', component: DashboardPage }) + +const leaderboardRoute = new Route({ + getParentRoute: () => rootRoute, + path: '/leaderboard', +}) +const leaderboardIndexRoute = new Route({ + getParentRoute: () => leaderboardRoute, + path: '/', + beforeLoad: ({ navigate }) => { + navigate({ to: '$discipline', params: { discipline: DEFAULT_DISCIPLINE }, replace: true }) + }, +}) +export const leaderboardDisciplineRoute = new Route({ + getParentRoute: () => leaderboardRoute, + path: '$discipline', + loader: ({ params, navigate }) => { + if (!isDiscipline(params.discipline)) { + navigate({ to: '../', replace: true }) + } + }, + component: () => ( + + + + ), +}) + +const allContestsRoute = new Route({ getParentRoute: () => rootRoute, path: '/contest' }) +const allContestsIndexRoute = new Route({ + getParentRoute: () => allContestsRoute, + path: '/', + beforeLoad: async ({ navigate }) => { + navigate({ to: '$contestNumber', params: { contestNumber: await getOngoingContestNumber() }, replace: true }) + }, +}) +const contestRoute = new Route({ + getParentRoute: () => allContestsRoute, + path: '$contestNumber', +}) +const contestIndexRoute = new Route({ + getParentRoute: () => contestRoute, + path: '/', + beforeLoad: ({ navigate }) => { + navigate({ to: '$discipline', params: { discipline: DEFAULT_DISCIPLINE }, replace: true }) + }, +}) + +export const contestDisciplineRoute = new Route({ + getParentRoute: () => contestRoute, + path: '$discipline', + loader: ({ params, navigate }) => { + const contestNumber = Number(params.contestNumber) + if (isNaN(contestNumber)) { + navigate({ to: '../../', replace: true }) + } + if (!isDiscipline(params.discipline)) { + navigate({ to: '../', replace: true }) + } + }, + component: () => ( + + + + ), +}) + +const devResetSessionRoute = new Route({ + getParentRoute: () => rootRoute, + path: 'dev/reset-session', + component: DevResetSession, +}) + +const routeTree = rootRoute.addChildren([ + indexRoute, + leaderboardRoute.addChildren([leaderboardIndexRoute, leaderboardDisciplineRoute]), + allContestsRoute.addChildren([ + allContestsIndexRoute, + contestRoute.addChildren([contestDisciplineRoute, contestIndexRoute]), + ]), + devResetSessionRoute, +]) +export const router = new Router({ routeTree }) + +declare module '@tanstack/react-router' { + interface Register { + router: typeof router + } +} From f9220cb0b5fedbc44cee14ddf64f0060a4d23181 Mon Sep 17 00:00:00 2001 From: bohdancho Date: Sat, 23 Dec 2023 17:43:09 +0100 Subject: [PATCH 03/27] refactor: move leaderboard and contests routers to separate files --- src/api/contests/useLeaderboard.ts | 15 +--- src/pages/contest/ContestDiscipline.tsx | 2 +- src/pages/contest/contestsRoute.tsx | 50 +++++++++++ src/pages/contest/index.ts | 2 +- .../leaderboard/LeaderboardDiscipline.tsx | 9 +- src/pages/leaderboard/index.ts | 2 +- src/pages/leaderboard/leaderboardRoute.tsx | 35 ++++++++ src/router.tsx | 85 ++----------------- 8 files changed, 100 insertions(+), 100 deletions(-) create mode 100644 src/pages/contest/contestsRoute.tsx create mode 100644 src/pages/leaderboard/leaderboardRoute.tsx diff --git a/src/api/contests/useLeaderboard.ts b/src/api/contests/useLeaderboard.ts index 96243661..0d1d1ade 100644 --- a/src/api/contests/useLeaderboard.ts +++ b/src/api/contests/useLeaderboard.ts @@ -1,4 +1,3 @@ -import useSWRImmutable from 'swr/immutable' import { axiosClient } from '../axios' import { Discipline, Scramble } from '@/types' @@ -11,17 +10,9 @@ export type LeaderboardResponse = Array<{ user: { id: number; username: string } contest: { contestNumber: number } }> - const API_ROUTE = 'contests/leaderboard/' -export function useLeaderboard(discipline: Discipline) { - const { data, error, isLoading } = useSWRImmutable<{ data: LeaderboardResponse }>( - `${API_ROUTE}${discipline}/`, - axiosClient.get, - ) - return { - data: data?.data, - isLoading, - isError: error, - } +export async function fetchLeaderboard(discipline: Discipline) { + const res = await axiosClient.get(`${API_ROUTE}${discipline}/`) + return res.data } diff --git a/src/pages/contest/ContestDiscipline.tsx b/src/pages/contest/ContestDiscipline.tsx index 6a84bb2e..550c7a7c 100644 --- a/src/pages/contest/ContestDiscipline.tsx +++ b/src/pages/contest/ContestDiscipline.tsx @@ -4,7 +4,7 @@ import { PublishedSession } from './components/PublishedSession' import { SolveContest } from './components/SolveContest' import { InfoBox } from '@/components' import { useUser } from '@/api/accounts' -import { contestDisciplineRoute } from '@/router' +import { contestDisciplineRoute } from './contestsRoute' export function ContestDiscipline() { const { userData } = useUser() diff --git a/src/pages/contest/contestsRoute.tsx b/src/pages/contest/contestsRoute.tsx new file mode 100644 index 00000000..e6dd121f --- /dev/null +++ b/src/pages/contest/contestsRoute.tsx @@ -0,0 +1,50 @@ +import { getOngoingContestNumber } from '@/api/contests' +import { DisciplinesTabsLayout } from '@/components' +import { rootRoute } from '@/router' +import { DEFAULT_DISCIPLINE, isDiscipline } from '@/types' +import { Route } from '@tanstack/react-router' +import { ContestDiscipline } from './ContestDiscipline' + +const allContestsRoute = new Route({ getParentRoute: () => rootRoute, path: '/contest' }) +const allContestsIndexRoute = new Route({ + getParentRoute: () => allContestsRoute, + path: '/', + beforeLoad: async ({ navigate }) => { + navigate({ to: '$contestNumber', params: { contestNumber: await getOngoingContestNumber() }, replace: true }) + }, +}) +const contestRoute = new Route({ + getParentRoute: () => allContestsRoute, + path: '$contestNumber', +}) +const contestIndexRoute = new Route({ + getParentRoute: () => contestRoute, + path: '/', + beforeLoad: ({ navigate }) => { + navigate({ to: '$discipline', params: { discipline: DEFAULT_DISCIPLINE }, replace: true }) + }, +}) + +export const contestDisciplineRoute = new Route({ + getParentRoute: () => contestRoute, + path: '$discipline', + loader: ({ params, navigate }) => { + const contestNumber = Number(params.contestNumber) + if (isNaN(contestNumber)) { + navigate({ to: '../../', replace: true }) + } + if (!isDiscipline(params.discipline)) { + navigate({ to: '../', replace: true }) + } + }, + component: () => ( + + + + ), +}) + +export default allContestsRoute.addChildren([ + allContestsIndexRoute, + contestRoute.addChildren([contestDisciplineRoute, contestIndexRoute]), +]) diff --git a/src/pages/contest/index.ts b/src/pages/contest/index.ts index a61bace1..75237131 100644 --- a/src/pages/contest/index.ts +++ b/src/pages/contest/index.ts @@ -1 +1 @@ -export * from './ContestDiscipline' +export * from './contestsRoute' diff --git a/src/pages/leaderboard/LeaderboardDiscipline.tsx b/src/pages/leaderboard/LeaderboardDiscipline.tsx index 85bef760..43c106d5 100644 --- a/src/pages/leaderboard/LeaderboardDiscipline.tsx +++ b/src/pages/leaderboard/LeaderboardDiscipline.tsx @@ -1,17 +1,14 @@ import { ReconstructTimeButton, ResultCard } from '@/components' -import { Discipline } from '@/types' -import { useLeaderboard, LeaderboardResponse } from '@/api/contests' +import { LeaderboardResponse } from '@/api/contests' import { useUser } from '@/api/accounts' import { useReconstructor } from '@/integrations/reconstructor' import { Link } from '@tanstack/react-router' -import { leaderboardDisciplineRoute } from '@/router' +import { leaderboardDisciplineRoute } from './leaderboardRoute' export function LeaderboardDiscipline() { const { userData } = useUser() - const params = leaderboardDisciplineRoute.useParams() - const discipline = params.discipline as Discipline // TODO add type guard - const { data: results } = useLeaderboard(discipline) + const results = leaderboardDisciplineRoute.useLoaderData() if (!results) { return 'loading...' diff --git a/src/pages/leaderboard/index.ts b/src/pages/leaderboard/index.ts index 7bfb9a1e..45d67e96 100644 --- a/src/pages/leaderboard/index.ts +++ b/src/pages/leaderboard/index.ts @@ -1 +1 @@ -export * from './LeaderboardDiscipline' +export * from './leaderboardRoute' diff --git a/src/pages/leaderboard/leaderboardRoute.tsx b/src/pages/leaderboard/leaderboardRoute.tsx new file mode 100644 index 00000000..f85776f5 --- /dev/null +++ b/src/pages/leaderboard/leaderboardRoute.tsx @@ -0,0 +1,35 @@ +import { fetchLeaderboard } from '@/api/contests' +import { rootRoute } from '@/router' +import { DEFAULT_DISCIPLINE, isDiscipline } from '@/types' +import { Route } from '@tanstack/react-router' +import { DisciplinesTabsLayout } from '@/components' +import { LeaderboardDiscipline } from './LeaderboardDiscipline' + +const leaderboardRoute = new Route({ + getParentRoute: () => rootRoute, + path: '/leaderboard', +}) +const leaderboardIndexRoute = new Route({ + getParentRoute: () => leaderboardRoute, + path: '/', + beforeLoad: ({ navigate }) => { + navigate({ to: '$discipline', params: { discipline: DEFAULT_DISCIPLINE }, replace: true }) + }, +}) +export const leaderboardDisciplineRoute = new Route({ + getParentRoute: () => leaderboardRoute, + path: '$discipline', + loader: ({ params, navigate }) => { + if (isDiscipline(params.discipline)) { + return fetchLeaderboard(params.discipline) + } + navigate({ to: '../', replace: true }) + }, + component: () => ( + + + + ), +}) + +export default leaderboardRoute.addChildren([leaderboardIndexRoute, leaderboardDisciplineRoute]) diff --git a/src/router.tsx b/src/router.tsx index 51f287d8..dc10f9da 100644 --- a/src/router.tsx +++ b/src/router.tsx @@ -1,11 +1,11 @@ -import { Layout, DisciplinesTabsLayout, DevResetSession } from './components' -import { DashboardPage, ContestDiscipline, LeaderboardDiscipline } from './pages' +import { Layout, DevResetSession } from './components' +import { DashboardPage } from './pages' import { TanStackRouterDevtools } from '@tanstack/router-devtools' -import { DEFAULT_DISCIPLINE, isDiscipline } from './types' -import { getOngoingContestNumber } from './api/contests' import { RootRoute, Route, Router } from '@tanstack/react-router' +import leaderboardRoute from './pages/leaderboard/leaderboardRoute' +import contestsRoute from './pages/contest/contestsRoute' -const rootRoute = new RootRoute({ +export const rootRoute = new RootRoute({ component: () => ( <> @@ -15,86 +15,13 @@ const rootRoute = new RootRoute({ }) const indexRoute = new Route({ getParentRoute: () => rootRoute, path: '/', component: DashboardPage }) -const leaderboardRoute = new Route({ - getParentRoute: () => rootRoute, - path: '/leaderboard', -}) -const leaderboardIndexRoute = new Route({ - getParentRoute: () => leaderboardRoute, - path: '/', - beforeLoad: ({ navigate }) => { - navigate({ to: '$discipline', params: { discipline: DEFAULT_DISCIPLINE }, replace: true }) - }, -}) -export const leaderboardDisciplineRoute = new Route({ - getParentRoute: () => leaderboardRoute, - path: '$discipline', - loader: ({ params, navigate }) => { - if (!isDiscipline(params.discipline)) { - navigate({ to: '../', replace: true }) - } - }, - component: () => ( - - - - ), -}) - -const allContestsRoute = new Route({ getParentRoute: () => rootRoute, path: '/contest' }) -const allContestsIndexRoute = new Route({ - getParentRoute: () => allContestsRoute, - path: '/', - beforeLoad: async ({ navigate }) => { - navigate({ to: '$contestNumber', params: { contestNumber: await getOngoingContestNumber() }, replace: true }) - }, -}) -const contestRoute = new Route({ - getParentRoute: () => allContestsRoute, - path: '$contestNumber', -}) -const contestIndexRoute = new Route({ - getParentRoute: () => contestRoute, - path: '/', - beforeLoad: ({ navigate }) => { - navigate({ to: '$discipline', params: { discipline: DEFAULT_DISCIPLINE }, replace: true }) - }, -}) - -export const contestDisciplineRoute = new Route({ - getParentRoute: () => contestRoute, - path: '$discipline', - loader: ({ params, navigate }) => { - const contestNumber = Number(params.contestNumber) - if (isNaN(contestNumber)) { - navigate({ to: '../../', replace: true }) - } - if (!isDiscipline(params.discipline)) { - navigate({ to: '../', replace: true }) - } - }, - component: () => ( - - - - ), -}) - const devResetSessionRoute = new Route({ getParentRoute: () => rootRoute, path: 'dev/reset-session', component: DevResetSession, }) -const routeTree = rootRoute.addChildren([ - indexRoute, - leaderboardRoute.addChildren([leaderboardIndexRoute, leaderboardDisciplineRoute]), - allContestsRoute.addChildren([ - allContestsIndexRoute, - contestRoute.addChildren([contestDisciplineRoute, contestIndexRoute]), - ]), - devResetSessionRoute, -]) +const routeTree = rootRoute.addChildren([indexRoute, leaderboardRoute, contestsRoute, devResetSessionRoute]) export const router = new Router({ routeTree }) declare module '@tanstack/react-router' { From ecc7a097ecdff94d1fe586e59cfe833b945513c4 Mon Sep 17 00:00:00 2001 From: bohdancho Date: Sat, 23 Dec 2023 19:16:02 +0100 Subject: [PATCH 04/27] refactor: add reactQuery, migrate leaderboard and ongoingContestNumber --- .eslintrc.cjs | 7 +++++- bun.lockb | Bin 137776 -> 143834 bytes package.json | 3 +++ src/App.tsx | 18 +++++++++------ src/api/contests/index.ts | 2 +- .../{useLeaderboard.ts => leaderboard.ts} | 9 +++++++- src/api/contests/ongoingContestNumber.ts | 17 ++++++-------- src/api/reactQuery.ts | 12 ++++++++++ src/components/layout/components/Navbar.tsx | 5 +++-- src/pages/contest/contestsRoute.tsx | 10 ++++++--- .../leaderboard/LeaderboardDiscipline.tsx | 7 ++++-- src/pages/leaderboard/leaderboardRoute.tsx | 12 +++++----- src/router.tsx | 21 ++++++++++++++---- 13 files changed, 87 insertions(+), 36 deletions(-) rename src/api/contests/{useLeaderboard.ts => leaderboard.ts} (62%) create mode 100644 src/api/reactQuery.ts diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 42947db1..2124d989 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -2,7 +2,12 @@ const config = { root: true, env: { browser: true, es2020: true, node: true }, - extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:react-hooks/recommended'], + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:react-hooks/recommended', + 'plugin:@tanstack/eslint-plugin-query/recommended', + ], ignorePatterns: ['dist', 'node_modules', '!.*.*'], parser: '@typescript-eslint/parser', plugins: ['react-refresh'], diff --git a/bun.lockb b/bun.lockb index 229059279ff13ecc2aaa9337688af76231fe44ca..26af03a0aeb014e1419a0c2de00433ebcfaa798e 100755 GIT binary patch delta 32039 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

    i!7MQ~SZHDfpyh%6_o{SUu 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 delta 27946 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 hx{BRxTdu5 diff --git a/package.json b/package.json index 7f67a8ec..7ff8190c 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@react-oauth/google": "^0.11.1", + "@tanstack/react-query": "^5.14.6", "@tanstack/react-router": "beta", "@typescript-eslint/parser": "^6.7.3", "axios": "^1.5.1", @@ -26,6 +27,8 @@ "usehooks-ts": "^2.9.1" }, "devDependencies": { + "@tanstack/eslint-plugin-query": "^5.14.6", + "@tanstack/react-query-devtools": "^5.14.6", "@tanstack/router-devtools": "beta", "@types/eslint": "^8.44.3", "@types/node": "^20.8.7", diff --git a/src/App.tsx b/src/App.tsx index 854706de..b9160d78 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,15 +4,19 @@ import { RouterProvider } from '@tanstack/react-router' import { CubeProvider } from './integrations/cube' import { ReconstructorProvider } from './integrations/reconstructor' import { router } from './router' +import { QueryClientProvider } from '@tanstack/react-query' +import { queryClient } from './api/reactQuery' export function App() { return ( - - - - - - - + + + + + + + + + ) } diff --git a/src/api/contests/index.ts b/src/api/contests/index.ts index 94e11582..4f439e8c 100644 --- a/src/api/contests/index.ts +++ b/src/api/contests/index.ts @@ -3,4 +3,4 @@ export * from './useContestResults' export * from './ongoingContestNumber' export * from './useSolveReconstruction' export * from './solveContest' -export * from './useLeaderboard' +export * from './leaderboard' diff --git a/src/api/contests/useLeaderboard.ts b/src/api/contests/leaderboard.ts similarity index 62% rename from src/api/contests/useLeaderboard.ts rename to src/api/contests/leaderboard.ts index 0d1d1ade..9f54b5c9 100644 --- a/src/api/contests/useLeaderboard.ts +++ b/src/api/contests/leaderboard.ts @@ -1,3 +1,4 @@ +import { queryOptions } from '@tanstack/react-query' import { axiosClient } from '../axios' import { Discipline, Scramble } from '@/types' @@ -12,7 +13,13 @@ export type LeaderboardResponse = Array<{ }> const API_ROUTE = 'contests/leaderboard/' -export async function fetchLeaderboard(discipline: Discipline) { +async function fetchLeaderboard(discipline: Discipline) { const res = await axiosClient.get(`${API_ROUTE}${discipline}/`) return res.data } + +export const leaderboardQueryOptions = (discipline: Discipline) => + queryOptions({ + queryKey: ['leaderboard', { discipline }], + queryFn: () => fetchLeaderboard(discipline), + }) diff --git a/src/api/contests/ongoingContestNumber.ts b/src/api/contests/ongoingContestNumber.ts index e9841b4c..3803dc96 100644 --- a/src/api/contests/ongoingContestNumber.ts +++ b/src/api/contests/ongoingContestNumber.ts @@ -1,16 +1,13 @@ -import useSWRImmutable from 'swr/immutable' import { axiosClient } from '../axios' - -type Response = number +import { queryOptions } from '@tanstack/react-query' const API_ROUTE = '/contests/ongoing-contest-number/' -export async function getOngoingContestNumber() { - const res = await axiosClient.get(API_ROUTE) +async function fetchOngoingContestNumber() { + const res = await axiosClient.get(API_ROUTE) return res.data } -export function useOngoingContestNumber() { - const { data, error, isLoading } = useSWRImmutable<{ data: Response }>(API_ROUTE, axiosClient.get) - - return { data: data?.data, isLoading, isError: error } -} +export const ongoingContestNumberQuery = queryOptions({ + queryKey: ['ongoing-contest-number'], + queryFn: () => fetchOngoingContestNumber(), +}) diff --git a/src/api/reactQuery.ts b/src/api/reactQuery.ts new file mode 100644 index 00000000..2b0c3314 --- /dev/null +++ b/src/api/reactQuery.ts @@ -0,0 +1,12 @@ +import { QueryClient } from '@tanstack/react-query' + +export const queryClient = new QueryClient({ + defaultOptions: { + queries: { + refetchOnWindowFocus: false, + refetchOnMount: false, + staleTime: Infinity, + retry: false, + }, + }, +}) diff --git a/src/components/layout/components/Navbar.tsx b/src/components/layout/components/Navbar.tsx index f61b328d..a034ed46 100644 --- a/src/components/layout/components/Navbar.tsx +++ b/src/components/layout/components/Navbar.tsx @@ -1,10 +1,11 @@ -import { useOngoingContestNumber } from '@/api/contests' +import { ongoingContestNumberQuery } from '@/api/contests' import { cn } from '@/utils' +import { useQuery } from '@tanstack/react-query' import { Link, useParams } from '@tanstack/react-router' import { ButtonHTMLAttributes, useMemo, useState } from 'react' export function NavBar() { - const { data: ongoingContestNumber } = useOngoingContestNumber() + const { data: ongoingContestNumber } = useQuery(ongoingContestNumberQuery) const params = useParams({ strict: false }) const openedContestNumber = params?.contestNumber ? Number(params?.contestNumber) : null const [isMobileNavVisible, setIsMovileNavVisible] = useState(false) diff --git a/src/pages/contest/contestsRoute.tsx b/src/pages/contest/contestsRoute.tsx index e6dd121f..5c2b1a87 100644 --- a/src/pages/contest/contestsRoute.tsx +++ b/src/pages/contest/contestsRoute.tsx @@ -1,16 +1,20 @@ -import { getOngoingContestNumber } from '@/api/contests' import { DisciplinesTabsLayout } from '@/components' import { rootRoute } from '@/router' import { DEFAULT_DISCIPLINE, isDiscipline } from '@/types' import { Route } from '@tanstack/react-router' import { ContestDiscipline } from './ContestDiscipline' +import { ongoingContestNumberQuery } from '@/api/contests' const allContestsRoute = new Route({ getParentRoute: () => rootRoute, path: '/contest' }) const allContestsIndexRoute = new Route({ getParentRoute: () => allContestsRoute, path: '/', - beforeLoad: async ({ navigate }) => { - navigate({ to: '$contestNumber', params: { contestNumber: await getOngoingContestNumber() }, replace: true }) + beforeLoad: async ({ navigate, context: { queryClient } }) => { + navigate({ + to: '$contestNumber', + params: { contestNumber: await queryClient.fetchQuery(ongoingContestNumberQuery) }, + replace: true, + }) }, }) const contestRoute = new Route({ diff --git a/src/pages/leaderboard/LeaderboardDiscipline.tsx b/src/pages/leaderboard/LeaderboardDiscipline.tsx index 43c106d5..d27cbf8d 100644 --- a/src/pages/leaderboard/LeaderboardDiscipline.tsx +++ b/src/pages/leaderboard/LeaderboardDiscipline.tsx @@ -1,14 +1,17 @@ import { ReconstructTimeButton, ResultCard } from '@/components' -import { LeaderboardResponse } from '@/api/contests' +import { LeaderboardResponse, leaderboardQueryOptions } from '@/api/contests' import { useUser } from '@/api/accounts' import { useReconstructor } from '@/integrations/reconstructor' import { Link } from '@tanstack/react-router' import { leaderboardDisciplineRoute } from './leaderboardRoute' +import { Discipline } from '@/types' +import { useSuspenseQuery } from '@tanstack/react-query' export function LeaderboardDiscipline() { const { userData } = useUser() - const results = leaderboardDisciplineRoute.useLoaderData() + const { discipline } = leaderboardDisciplineRoute.useParams() + const { data: results } = useSuspenseQuery(leaderboardQueryOptions(discipline as Discipline)) if (!results) { return 'loading...' diff --git a/src/pages/leaderboard/leaderboardRoute.tsx b/src/pages/leaderboard/leaderboardRoute.tsx index f85776f5..f3e7c823 100644 --- a/src/pages/leaderboard/leaderboardRoute.tsx +++ b/src/pages/leaderboard/leaderboardRoute.tsx @@ -1,4 +1,4 @@ -import { fetchLeaderboard } from '@/api/contests' +import { leaderboardQueryOptions } from '@/api/contests' import { rootRoute } from '@/router' import { DEFAULT_DISCIPLINE, isDiscipline } from '@/types' import { Route } from '@tanstack/react-router' @@ -19,11 +19,13 @@ const leaderboardIndexRoute = new Route({ export const leaderboardDisciplineRoute = new Route({ getParentRoute: () => leaderboardRoute, path: '$discipline', - loader: ({ params, navigate }) => { - if (isDiscipline(params.discipline)) { - return fetchLeaderboard(params.discipline) + pendingComponent: () =>

    Loading...
    , + loader: ({ params: { discipline }, navigate, context: { queryClient } }) => { + if (!isDiscipline(discipline)) { + navigate({ to: '../', replace: true }) + return } - navigate({ to: '../', replace: true }) + queryClient.ensureQueryData(leaderboardQueryOptions(discipline)) }, component: () => ( diff --git a/src/router.tsx b/src/router.tsx index dc10f9da..7db7f173 100644 --- a/src/router.tsx +++ b/src/router.tsx @@ -1,15 +1,22 @@ import { Layout, DevResetSession } from './components' import { DashboardPage } from './pages' import { TanStackRouterDevtools } from '@tanstack/router-devtools' -import { RootRoute, Route, Router } from '@tanstack/react-router' +import { Route, Router, rootRouteWithContext } from '@tanstack/react-router' import leaderboardRoute from './pages/leaderboard/leaderboardRoute' import contestsRoute from './pages/contest/contestsRoute' +import { QueryClient } from '@tanstack/react-query' +import { queryClient } from './api/reactQuery' +import { ReactQueryDevtools } from '@tanstack/react-query-devtools' -export const rootRoute = new RootRoute({ +export const rootRoute = rootRouteWithContext<{ queryClient: QueryClient }>()({ component: () => ( <> - {import.meta.env.MODE === 'development' && } + {import.meta.env.MODE === 'development' && ( + <> + + + )} ), }) @@ -22,7 +29,13 @@ const devResetSessionRoute = new Route({ }) const routeTree = rootRoute.addChildren([indexRoute, leaderboardRoute, contestsRoute, devResetSessionRoute]) -export const router = new Router({ routeTree }) +export const router = new Router({ + routeTree, + defaultPreloadStaleTime: 0, + context: { + queryClient, + }, +}) declare module '@tanstack/react-router' { interface Register { From c9c51e0ecd1f70b61aecc0b4a9814d3fb54ea7e3 Mon Sep 17 00:00:00 2001 From: bohdancho Date: Sat, 23 Dec 2023 19:37:37 +0100 Subject: [PATCH 05/27] refactor: migrate dashboard to reactQuery --- src/api/contests/{useDashboard.ts => dashboard.ts} | 14 ++++++-------- src/api/contests/leaderboard.ts | 2 +- src/pages/dashboard/DashboardPage.tsx | 13 +++++++++---- src/pages/leaderboard/LeaderboardDiscipline.tsx | 8 ++------ src/pages/leaderboard/leaderboardRoute.tsx | 4 ++-- src/router.tsx | 9 +++++++-- 6 files changed, 27 insertions(+), 23 deletions(-) rename src/api/contests/{useDashboard.ts => dashboard.ts} (62%) diff --git a/src/api/contests/useDashboard.ts b/src/api/contests/dashboard.ts similarity index 62% rename from src/api/contests/useDashboard.ts rename to src/api/contests/dashboard.ts index 29a432dc..67f9c1b0 100644 --- a/src/api/contests/useDashboard.ts +++ b/src/api/contests/dashboard.ts @@ -1,6 +1,6 @@ -import useSWRImmutable from 'swr/immutable' import { axiosClient } from '../axios' import { Discipline } from '@/types' +import { queryOptions } from '@tanstack/react-query' export type DashboardResponse = { bestSolves: Array<{ @@ -20,12 +20,10 @@ export type DashboardResponse = { } const API_ROUTE = 'contests/dashboard/' -export function useDashboard() { - const { data, error, isLoading } = useSWRImmutable<{ data: DashboardResponse }>(API_ROUTE, axiosClient.get) - return { - data: data?.data, - isLoading, - isError: error, - } +async function fetchDashboard() { + const res = await axiosClient.get(API_ROUTE) + return res.data } + +export const dashboardQuery = queryOptions({ queryKey: ['dashboard'], queryFn: fetchDashboard }) diff --git a/src/api/contests/leaderboard.ts b/src/api/contests/leaderboard.ts index 9f54b5c9..99a2ba5f 100644 --- a/src/api/contests/leaderboard.ts +++ b/src/api/contests/leaderboard.ts @@ -18,7 +18,7 @@ async function fetchLeaderboard(discipline: Discipline) { return res.data } -export const leaderboardQueryOptions = (discipline: Discipline) => +export const leaderboardQuery = (discipline: Discipline) => queryOptions({ queryKey: ['leaderboard', { discipline }], queryFn: () => fetchLeaderboard(discipline), diff --git a/src/pages/dashboard/DashboardPage.tsx b/src/pages/dashboard/DashboardPage.tsx index 79879e38..158cef8a 100644 --- a/src/pages/dashboard/DashboardPage.tsx +++ b/src/pages/dashboard/DashboardPage.tsx @@ -1,19 +1,24 @@ -import { useDashboard } from '@/api/contests' +import { dashboardQuery } from '@/api/contests' import { ContestsList } from './components/ContestsList' import { BestSolves } from './components/BestSolves' +import { QueryClient, useQuery } from '@tanstack/react-query' + +export function dashboardLoader({ context: { queryClient } }: { context: { queryClient: QueryClient } }) { + queryClient.ensureQueryData(dashboardQuery) +} export function DashboardPage() { - const { data } = useDashboard() + const { data: dashboard } = useQuery(dashboardQuery) return (

    Contests

    - + {dashboard?.contests ? : 'Loading...'}

    Best Solves

    - + {dashboard?.bestSolves ? : 'Loading...'}
    ) diff --git a/src/pages/leaderboard/LeaderboardDiscipline.tsx b/src/pages/leaderboard/LeaderboardDiscipline.tsx index d27cbf8d..cabaaa53 100644 --- a/src/pages/leaderboard/LeaderboardDiscipline.tsx +++ b/src/pages/leaderboard/LeaderboardDiscipline.tsx @@ -1,5 +1,5 @@ import { ReconstructTimeButton, ResultCard } from '@/components' -import { LeaderboardResponse, leaderboardQueryOptions } from '@/api/contests' +import { LeaderboardResponse, leaderboardQuery } from '@/api/contests' import { useUser } from '@/api/accounts' import { useReconstructor } from '@/integrations/reconstructor' import { Link } from '@tanstack/react-router' @@ -11,11 +11,7 @@ export function LeaderboardDiscipline() { const { userData } = useUser() const { discipline } = leaderboardDisciplineRoute.useParams() - const { data: results } = useSuspenseQuery(leaderboardQueryOptions(discipline as Discipline)) - - if (!results) { - return 'loading...' - } + const { data: results } = useSuspenseQuery(leaderboardQuery(discipline as Discipline)) const ownResult = userData && results.find((result) => result.user.username === userData.username) diff --git a/src/pages/leaderboard/leaderboardRoute.tsx b/src/pages/leaderboard/leaderboardRoute.tsx index f3e7c823..80e47599 100644 --- a/src/pages/leaderboard/leaderboardRoute.tsx +++ b/src/pages/leaderboard/leaderboardRoute.tsx @@ -1,4 +1,4 @@ -import { leaderboardQueryOptions } from '@/api/contests' +import { leaderboardQuery } from '@/api/contests' import { rootRoute } from '@/router' import { DEFAULT_DISCIPLINE, isDiscipline } from '@/types' import { Route } from '@tanstack/react-router' @@ -25,7 +25,7 @@ export const leaderboardDisciplineRoute = new Route({ navigate({ to: '../', replace: true }) return } - queryClient.ensureQueryData(leaderboardQueryOptions(discipline)) + queryClient.ensureQueryData(leaderboardQuery(discipline)) }, component: () => ( diff --git a/src/router.tsx b/src/router.tsx index 7db7f173..8fa884a5 100644 --- a/src/router.tsx +++ b/src/router.tsx @@ -1,5 +1,5 @@ import { Layout, DevResetSession } from './components' -import { DashboardPage } from './pages' +import { DashboardPage, dashboardLoader } from './pages' import { TanStackRouterDevtools } from '@tanstack/router-devtools' import { Route, Router, rootRouteWithContext } from '@tanstack/react-router' import leaderboardRoute from './pages/leaderboard/leaderboardRoute' @@ -20,7 +20,12 @@ export const rootRoute = rootRouteWithContext<{ queryClient: QueryClient }>()({ ), }) -const indexRoute = new Route({ getParentRoute: () => rootRoute, path: '/', component: DashboardPage }) +const indexRoute = new Route({ + getParentRoute: () => rootRoute, + path: '/', + component: DashboardPage, + loader: dashboardLoader, +}) const devResetSessionRoute = new Route({ getParentRoute: () => rootRoute, From d2c2080a10df3950bd01b4141c12d9ea0004c7c0 Mon Sep 17 00:00:00 2001 From: bohdancho Date: Sat, 23 Dec 2023 19:38:02 +0100 Subject: [PATCH 06/27] fix: adjust barrel file --- src/api/contests/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/contests/index.ts b/src/api/contests/index.ts index 4f439e8c..04929f08 100644 --- a/src/api/contests/index.ts +++ b/src/api/contests/index.ts @@ -1,4 +1,4 @@ -export * from './useDashboard' +export * from './dashboard' export * from './useContestResults' export * from './ongoingContestNumber' export * from './useSolveReconstruction' From 26f13f93aea958c62bfaa44d6ad9507752d90b5d Mon Sep 17 00:00:00 2001 From: bohdancho Date: Sat, 23 Dec 2023 19:53:02 +0100 Subject: [PATCH 07/27] refactor: leaderboard pass query from loader --- src/pages/leaderboard/LeaderboardDiscipline.tsx | 7 +++---- src/pages/leaderboard/leaderboardRoute.tsx | 4 ++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/pages/leaderboard/LeaderboardDiscipline.tsx b/src/pages/leaderboard/LeaderboardDiscipline.tsx index cabaaa53..5477034b 100644 --- a/src/pages/leaderboard/LeaderboardDiscipline.tsx +++ b/src/pages/leaderboard/LeaderboardDiscipline.tsx @@ -1,17 +1,16 @@ import { ReconstructTimeButton, ResultCard } from '@/components' -import { LeaderboardResponse, leaderboardQuery } from '@/api/contests' +import { LeaderboardResponse } from '@/api/contests' import { useUser } from '@/api/accounts' import { useReconstructor } from '@/integrations/reconstructor' import { Link } from '@tanstack/react-router' import { leaderboardDisciplineRoute } from './leaderboardRoute' -import { Discipline } from '@/types' import { useSuspenseQuery } from '@tanstack/react-query' export function LeaderboardDiscipline() { const { userData } = useUser() - const { discipline } = leaderboardDisciplineRoute.useParams() - const { data: results } = useSuspenseQuery(leaderboardQuery(discipline as Discipline)) + const query = leaderboardDisciplineRoute.useLoaderData() + const { data: results } = useSuspenseQuery(query) const ownResult = userData && results.find((result) => result.user.username === userData.username) diff --git a/src/pages/leaderboard/leaderboardRoute.tsx b/src/pages/leaderboard/leaderboardRoute.tsx index 80e47599..14f580ed 100644 --- a/src/pages/leaderboard/leaderboardRoute.tsx +++ b/src/pages/leaderboard/leaderboardRoute.tsx @@ -22,10 +22,10 @@ export const leaderboardDisciplineRoute = new Route({ pendingComponent: () =>
    Loading...
    , loader: ({ params: { discipline }, navigate, context: { queryClient } }) => { if (!isDiscipline(discipline)) { - navigate({ to: '../', replace: true }) - return + throw navigate({ to: '../', replace: true }) } queryClient.ensureQueryData(leaderboardQuery(discipline)) + return leaderboardQuery(discipline) }, component: () => ( From 7af8cc0d84b11102afb33a4c6884642e78ba287a Mon Sep 17 00:00:00 2001 From: bohdancho Date: Sat, 23 Dec 2023 20:54:58 +0100 Subject: [PATCH 08/27] refactor: migrate PublishedSessions to reactQuery --- src/api/contests/leaderboard.ts | 4 +++- src/api/contests/useContestResults.ts | 27 ++++++++-------------- src/api/reactQuery.ts | 8 ++++++- src/pages/contest/ContestDiscipline.tsx | 25 +++++++++++--------- src/pages/contest/contestsRoute.tsx | 12 ++++++---- src/pages/leaderboard/leaderboardRoute.tsx | 6 +++-- 6 files changed, 46 insertions(+), 36 deletions(-) diff --git a/src/api/contests/leaderboard.ts b/src/api/contests/leaderboard.ts index 99a2ba5f..e8a4ee43 100644 --- a/src/api/contests/leaderboard.ts +++ b/src/api/contests/leaderboard.ts @@ -1,6 +1,7 @@ import { queryOptions } from '@tanstack/react-query' import { axiosClient } from '../axios' import { Discipline, Scramble } from '@/types' +import { AxiosError } from 'axios' export type LeaderboardResponse = Array<{ id: number @@ -19,7 +20,8 @@ async function fetchLeaderboard(discipline: Discipline) { } export const leaderboardQuery = (discipline: Discipline) => - queryOptions({ + queryOptions({ queryKey: ['leaderboard', { discipline }], queryFn: () => fetchLeaderboard(discipline), + retry: false, }) diff --git a/src/api/contests/useContestResults.ts b/src/api/contests/useContestResults.ts index 047412dc..02f833ce 100644 --- a/src/api/contests/useContestResults.ts +++ b/src/api/contests/useContestResults.ts @@ -1,6 +1,6 @@ import { Discipline, Scramble } from '@/types' -import useSWRImmutable from 'swr/immutable' import { axiosClient } from '../axios' +import { queryOptions } from '@tanstack/react-query' export type ContestResultsResponse = Array<{ id: number @@ -15,22 +15,15 @@ export type ContestResultsResponse = Array<{ state: 'submitted' | 'changed_to_extra' }> }> - -type ContestResultsError = { - response: { status: number } -} - const API_ROUTE = 'contests/contest/' -export function useContestResults(contestNumber: number, discipline: Discipline) { - const { data, error, isLoading } = useSWRImmutable<{ data: ContestResultsResponse }, ContestResultsError>( - `${API_ROUTE}${contestNumber}/${discipline}/`, - axiosClient.get, - { shouldRetryOnError: false }, - ) - return { - data: data?.data, - isLoading, - error, - } +async function fetchContestResults(contestNumber: number, discipline: Discipline) { + const res = await axiosClient.get(`${API_ROUTE}${contestNumber}/${discipline}/`) + return res.data } + +export const contestResultsQuery = (contestNumber: number, discipline: Discipline) => + queryOptions({ + queryKey: ['contestResults', { contestNumber, discipline }], + queryFn: () => fetchContestResults(contestNumber, discipline), + }) diff --git a/src/api/reactQuery.ts b/src/api/reactQuery.ts index 2b0c3314..843f5e75 100644 --- a/src/api/reactQuery.ts +++ b/src/api/reactQuery.ts @@ -1,4 +1,5 @@ import { QueryClient } from '@tanstack/react-query' +import { AxiosError } from 'axios' export const queryClient = new QueryClient({ defaultOptions: { @@ -6,7 +7,12 @@ export const queryClient = new QueryClient({ refetchOnWindowFocus: false, refetchOnMount: false, staleTime: Infinity, - retry: false, }, }, }) + +declare module '@tanstack/react-query' { + interface Register { + defaultError: AxiosError + } +} diff --git a/src/pages/contest/ContestDiscipline.tsx b/src/pages/contest/ContestDiscipline.tsx index 550c7a7c..5a5bf264 100644 --- a/src/pages/contest/ContestDiscipline.tsx +++ b/src/pages/contest/ContestDiscipline.tsx @@ -1,33 +1,36 @@ -import { ContestResultsResponse, useContestResults } from '@/api/contests' -import { Discipline } from '@/types' +import { ContestResultsResponse } from '@/api/contests' import { PublishedSession } from './components/PublishedSession' import { SolveContest } from './components/SolveContest' import { InfoBox } from '@/components' import { useUser } from '@/api/accounts' import { contestDisciplineRoute } from './contestsRoute' +import { useQuery } from '@tanstack/react-query' +import { Discipline } from '@/types' export function ContestDiscipline() { const { userData } = useUser() const routeParams = contestDisciplineRoute.useParams() - - const contestNumber = Number(routeParams.contestNumber) - const disciplineName = routeParams.discipline as Discipline // TODO add type guard - - const { data: sessions, error, isLoading } = useContestResults(contestNumber, disciplineName) + const query = contestDisciplineRoute.useLoaderData() + const { data: sessions, error, isLoading } = useQuery(query) if (isLoading) { return loading... } - if (error?.response.status === 403) { - return + if (error?.response?.status === 403) { + return ( + + ) } - if (error?.response.status === 401) { + if (error?.response?.status === 401) { return You need to be signed in to participate in a contest } - if (error || !sessions) { + if (error || sessions === undefined) { return An unknown error occured } diff --git a/src/pages/contest/contestsRoute.tsx b/src/pages/contest/contestsRoute.tsx index 5c2b1a87..9cb6b41d 100644 --- a/src/pages/contest/contestsRoute.tsx +++ b/src/pages/contest/contestsRoute.tsx @@ -3,7 +3,7 @@ import { rootRoute } from '@/router' import { DEFAULT_DISCIPLINE, isDiscipline } from '@/types' import { Route } from '@tanstack/react-router' import { ContestDiscipline } from './ContestDiscipline' -import { ongoingContestNumberQuery } from '@/api/contests' +import { contestResultsQuery, ongoingContestNumberQuery } from '@/api/contests' const allContestsRoute = new Route({ getParentRoute: () => rootRoute, path: '/contest' }) const allContestsIndexRoute = new Route({ @@ -32,14 +32,18 @@ const contestIndexRoute = new Route({ export const contestDisciplineRoute = new Route({ getParentRoute: () => contestRoute, path: '$discipline', - loader: ({ params, navigate }) => { + loader: ({ params, navigate, context: { queryClient } }) => { const contestNumber = Number(params.contestNumber) if (isNaN(contestNumber)) { - navigate({ to: '../../', replace: true }) + throw navigate({ to: '../../', replace: true }) } if (!isDiscipline(params.discipline)) { - navigate({ to: '../', replace: true }) + throw navigate({ to: '../', replace: true }) } + + const query = contestResultsQuery(contestNumber, params.discipline) + queryClient.ensureQueryData(query) + return query }, component: () => ( diff --git a/src/pages/leaderboard/leaderboardRoute.tsx b/src/pages/leaderboard/leaderboardRoute.tsx index 14f580ed..3923b707 100644 --- a/src/pages/leaderboard/leaderboardRoute.tsx +++ b/src/pages/leaderboard/leaderboardRoute.tsx @@ -24,8 +24,10 @@ export const leaderboardDisciplineRoute = new Route({ if (!isDiscipline(discipline)) { throw navigate({ to: '../', replace: true }) } - queryClient.ensureQueryData(leaderboardQuery(discipline)) - return leaderboardQuery(discipline) + + const query = leaderboardQuery(discipline) + queryClient.ensureQueryData(query) + return query }, component: () => ( From 26edec57f87b406409f6e4856e0d3bad26c16cf7 Mon Sep 17 00:00:00 2001 From: bohdancho Date: Sat, 23 Dec 2023 21:42:38 +0100 Subject: [PATCH 09/27] refactor: migrate useUser to reactQuery --- src/api/accounts/index.ts | 2 +- src/api/accounts/useUser.ts | 15 -------------- src/api/accounts/user.ts | 20 +++++++++++++++++++ src/api/auth.ts | 7 ++++--- ...useContestResults.ts => contestResults.ts} | 8 ++++++-- src/api/contests/index.ts | 2 +- src/api/contests/leaderboard.ts | 3 +-- src/components/PickUsernameModal.tsx | 5 +++-- .../layout/components/LoginSection.tsx | 5 +++-- src/pages/contest/ContestDiscipline.tsx | 4 ++-- .../components/SolveContest/SolveContest.tsx | 5 +++-- .../leaderboard/LeaderboardDiscipline.tsx | 6 +++--- 12 files changed, 47 insertions(+), 35 deletions(-) delete mode 100644 src/api/accounts/useUser.ts create mode 100644 src/api/accounts/user.ts rename src/api/contests/{useContestResults.ts => contestResults.ts} (76%) diff --git a/src/api/accounts/index.ts b/src/api/accounts/index.ts index 90bcb4eb..657e6ed3 100644 --- a/src/api/accounts/index.ts +++ b/src/api/accounts/index.ts @@ -1,4 +1,4 @@ -export * from './useUser' +export * from './user' export * from './postLogin' export * from './refreshAccessToken' export * from './putChangeUsername' diff --git a/src/api/accounts/useUser.ts b/src/api/accounts/useUser.ts deleted file mode 100644 index fc4797f1..00000000 --- a/src/api/accounts/useUser.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { axiosClient } from '../axios' -import useSWRImmutable from 'swr/immutable' - -const API_ROUTE = 'accounts/current_user/' -type UserData = { username: string; authCompleted: boolean } - -export function useUser() { - const { data, error, isLoading } = useSWRImmutable<{ data: UserData }>(API_ROUTE, axiosClient.get) - - return { - userData: data ? data.data : null, - isLoading, - isError: error, - } -} diff --git a/src/api/accounts/user.ts b/src/api/accounts/user.ts new file mode 100644 index 00000000..8a06256a --- /dev/null +++ b/src/api/accounts/user.ts @@ -0,0 +1,20 @@ +import { queryOptions } from '@tanstack/react-query' +import { axiosClient } from '../axios' + +const API_ROUTE = 'accounts/current_user/' +type UserData = { username: string; authCompleted: boolean } + +async function fetchUser() { + try { + const res = await axiosClient.get(API_ROUTE) + return res.data + } catch (err) { + return null + } +} + +export const USER_QUERY_KEY = 'user' +export const userQuery = queryOptions({ + queryKey: [USER_QUERY_KEY], + queryFn: fetchUser, +}) diff --git a/src/api/auth.ts b/src/api/auth.ts index 142e7ffb..9dc7c88f 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -1,15 +1,16 @@ -import { postLogin } from '@/api/accounts' +import { USER_QUERY_KEY, postLogin } from '@/api/accounts' import { setAuthTokens, deleteAuthTokens } from './authTokens' +import { queryClient } from './reactQuery' export async function login(googleCode: string) { const response = await postLogin(googleCode) const { refresh, access } = response.data setAuthTokens({ refresh, access }) - window.location.reload() + queryClient.invalidateQueries({ queryKey: [USER_QUERY_KEY] }) } export function logout() { deleteAuthTokens() - window.location.reload() + queryClient.invalidateQueries({ queryKey: [USER_QUERY_KEY] }) } diff --git a/src/api/contests/useContestResults.ts b/src/api/contests/contestResults.ts similarity index 76% rename from src/api/contests/useContestResults.ts rename to src/api/contests/contestResults.ts index 02f833ce..fd79b6ec 100644 --- a/src/api/contests/useContestResults.ts +++ b/src/api/contests/contestResults.ts @@ -1,6 +1,8 @@ import { Discipline, Scramble } from '@/types' import { axiosClient } from '../axios' import { queryOptions } from '@tanstack/react-query' +import { USER_QUERY_KEY } from '../accounts' +import { AxiosError } from 'axios' export type ContestResultsResponse = Array<{ id: number @@ -24,6 +26,8 @@ async function fetchContestResults(contestNumber: number, discipline: Discipline export const contestResultsQuery = (contestNumber: number, discipline: Discipline) => queryOptions({ - queryKey: ['contestResults', { contestNumber, discipline }], - queryFn: () => fetchContestResults(contestNumber, discipline), + queryKey: [USER_QUERY_KEY, 'contestResults', { contestNumber, discipline }], + queryFn: () => { + return fetchContestResults(contestNumber, discipline) + }, }) diff --git a/src/api/contests/index.ts b/src/api/contests/index.ts index 04929f08..2dc607d3 100644 --- a/src/api/contests/index.ts +++ b/src/api/contests/index.ts @@ -1,5 +1,5 @@ export * from './dashboard' -export * from './useContestResults' +export * from './contestResults' export * from './ongoingContestNumber' export * from './useSolveReconstruction' export * from './solveContest' diff --git a/src/api/contests/leaderboard.ts b/src/api/contests/leaderboard.ts index e8a4ee43..945ebc57 100644 --- a/src/api/contests/leaderboard.ts +++ b/src/api/contests/leaderboard.ts @@ -1,7 +1,6 @@ import { queryOptions } from '@tanstack/react-query' import { axiosClient } from '../axios' import { Discipline, Scramble } from '@/types' -import { AxiosError } from 'axios' export type LeaderboardResponse = Array<{ id: number @@ -20,7 +19,7 @@ async function fetchLeaderboard(discipline: Discipline) { } export const leaderboardQuery = (discipline: Discipline) => - queryOptions({ + queryOptions({ queryKey: ['leaderboard', { discipline }], queryFn: () => fetchLeaderboard(discipline), retry: false, diff --git a/src/components/PickUsernameModal.tsx b/src/components/PickUsernameModal.tsx index 80da4561..dc533de6 100644 --- a/src/components/PickUsernameModal.tsx +++ b/src/components/PickUsernameModal.tsx @@ -1,10 +1,11 @@ -import { putChangeUsername, useUser } from '@/api/accounts' +import { putChangeUsername, userQuery } from '@/api/accounts' +import { useQuery } from '@tanstack/react-query' import { useEffect, useState } from 'react' export function PickUsernameModal() { const [isVisible, setIsVisible] = useState(false) const [username, setUsername] = useState('') - const { userData } = useUser() + const { data: userData } = useQuery(userQuery) useEffect(() => { if (userData && !userData.authCompleted) { diff --git a/src/components/layout/components/LoginSection.tsx b/src/components/layout/components/LoginSection.tsx index 2a27702e..b13a554a 100644 --- a/src/components/layout/components/LoginSection.tsx +++ b/src/components/layout/components/LoginSection.tsx @@ -1,10 +1,11 @@ import { useGoogleLogin } from '@react-oauth/google' import googleLogo from '@/assets/google-logo.svg' -import { useUser } from '@/api/accounts' +import { userQuery } from '@/api/accounts' import { login, logout } from '@/api/auth' +import { useQuery } from '@tanstack/react-query' export function LoginSection() { - const { userData } = useUser() + const { data: userData } = useQuery(userQuery) const handleLogin = useGoogleLogin({ onSuccess: ({ code }) => login(code), diff --git a/src/pages/contest/ContestDiscipline.tsx b/src/pages/contest/ContestDiscipline.tsx index 5a5bf264..8d2d498e 100644 --- a/src/pages/contest/ContestDiscipline.tsx +++ b/src/pages/contest/ContestDiscipline.tsx @@ -2,13 +2,13 @@ import { ContestResultsResponse } from '@/api/contests' import { PublishedSession } from './components/PublishedSession' import { SolveContest } from './components/SolveContest' import { InfoBox } from '@/components' -import { useUser } from '@/api/accounts' +import { userQuery } from '@/api/accounts' import { contestDisciplineRoute } from './contestsRoute' import { useQuery } from '@tanstack/react-query' import { Discipline } from '@/types' export function ContestDiscipline() { - const { userData } = useUser() + const { data: userData } = useQuery(userQuery) const routeParams = contestDisciplineRoute.useParams() const query = contestDisciplineRoute.useLoaderData() const { data: sessions, error, isLoading } = useQuery(query) diff --git a/src/pages/contest/components/SolveContest/SolveContest.tsx b/src/pages/contest/components/SolveContest/SolveContest.tsx index 7047fb95..62598d79 100644 --- a/src/pages/contest/components/SolveContest/SolveContest.tsx +++ b/src/pages/contest/components/SolveContest/SolveContest.tsx @@ -2,12 +2,13 @@ import { Discipline } from '@/types' import { useSolveContestState, postSolveResult, changeToExtra, submitSolve } from '@/api/contests' import { CurrentSolve, SubmittedSolve } from './components' import { InfoBox } from '@/components' -import { useUser } from '@/api/accounts' +import { userQuery } from '@/api/accounts' import { CubeSolveResult } from '@/integrations/cube' +import { useQuery } from '@tanstack/react-query' type SolveContestProps = { contestNumber: number; discipline: Discipline } export function SolveContest({ contestNumber, discipline }: SolveContestProps) { - const { userData } = useUser() + const { data: userData } = useQuery(userQuery) const { data: state, mutate: mutateState } = useSolveContestState(contestNumber, discipline) if (userData && !userData.authCompleted) { diff --git a/src/pages/leaderboard/LeaderboardDiscipline.tsx b/src/pages/leaderboard/LeaderboardDiscipline.tsx index 5477034b..affdbf03 100644 --- a/src/pages/leaderboard/LeaderboardDiscipline.tsx +++ b/src/pages/leaderboard/LeaderboardDiscipline.tsx @@ -1,13 +1,13 @@ import { ReconstructTimeButton, ResultCard } from '@/components' import { LeaderboardResponse } from '@/api/contests' -import { useUser } from '@/api/accounts' +import { userQuery } from '@/api/accounts' import { useReconstructor } from '@/integrations/reconstructor' import { Link } from '@tanstack/react-router' +import { useQuery, useSuspenseQuery } from '@tanstack/react-query' import { leaderboardDisciplineRoute } from './leaderboardRoute' -import { useSuspenseQuery } from '@tanstack/react-query' export function LeaderboardDiscipline() { - const { userData } = useUser() + const { data: userData } = useQuery(userQuery) const query = leaderboardDisciplineRoute.useLoaderData() const { data: results } = useSuspenseQuery(query) From 54cac4601838cb69e9d8c53a3b0534dc9cee9c18 Mon Sep 17 00:00:00 2001 From: bohdancho Date: Sat, 23 Dec 2023 21:52:28 +0100 Subject: [PATCH 10/27] refactor: migrate useSolveReconstruction to reactQuery --- src/api/contests/contestResults.ts | 1 - src/api/contests/index.ts | 2 +- src/api/contests/solveReconstruction.ts | 26 +++++++++++++++++++ src/api/contests/useSolveReconstruction.ts | 21 --------------- src/components/DevResetSession.tsx | 2 ++ .../reconstructor/ReconstructorProvider.tsx | 5 ++-- 6 files changed, 32 insertions(+), 25 deletions(-) create mode 100644 src/api/contests/solveReconstruction.ts delete mode 100644 src/api/contests/useSolveReconstruction.ts diff --git a/src/api/contests/contestResults.ts b/src/api/contests/contestResults.ts index fd79b6ec..ba7da4a3 100644 --- a/src/api/contests/contestResults.ts +++ b/src/api/contests/contestResults.ts @@ -2,7 +2,6 @@ import { Discipline, Scramble } from '@/types' import { axiosClient } from '../axios' import { queryOptions } from '@tanstack/react-query' import { USER_QUERY_KEY } from '../accounts' -import { AxiosError } from 'axios' export type ContestResultsResponse = Array<{ id: number diff --git a/src/api/contests/index.ts b/src/api/contests/index.ts index 2dc607d3..9706ebbe 100644 --- a/src/api/contests/index.ts +++ b/src/api/contests/index.ts @@ -1,6 +1,6 @@ export * from './dashboard' export * from './contestResults' export * from './ongoingContestNumber' -export * from './useSolveReconstruction' +export * from './solveReconstruction' export * from './solveContest' export * from './leaderboard' diff --git a/src/api/contests/solveReconstruction.ts b/src/api/contests/solveReconstruction.ts new file mode 100644 index 00000000..424d435e --- /dev/null +++ b/src/api/contests/solveReconstruction.ts @@ -0,0 +1,26 @@ +import useSWRImmutable from 'swr/immutable' +import { axiosClient } from '../axios' +import { Discipline, Scramble } from '@/types' +import { queryOptions } from '@tanstack/react-query' + +const API_ROUTE = 'contests/solve-reconstruction/' +export type SolveReconstructionResponse = { + id: number + reconstruction: string + scramble: Pick + contestNumber: number + discipline: { name: Discipline } + user: { username: string } +} + +async function fetchSolveReconstruction(solveId: number) { + const res = await axiosClient.get(`${API_ROUTE}${solveId}/`) + return res.data +} + +export const solveReconstructionQuery = (solveId: number | null) => + queryOptions({ + queryKey: ['solveReconstruction', solveId], + queryFn: () => fetchSolveReconstruction(solveId as number), + enabled: typeof solveId === 'number', + }) diff --git a/src/api/contests/useSolveReconstruction.ts b/src/api/contests/useSolveReconstruction.ts deleted file mode 100644 index 55f6b619..00000000 --- a/src/api/contests/useSolveReconstruction.ts +++ /dev/null @@ -1,21 +0,0 @@ -import useSWRImmutable from 'swr/immutable' -import { axiosClient } from '../axios' -import { Discipline, Scramble } from '@/types' - -const API_ROUTE = 'contests/solve-reconstruction/' -export type SolveReconstructionResponse = { - id: number - reconstruction: string - scramble: Pick - contestNumber: number - discipline: { name: Discipline } - user: { username: string } -} - -export function useSolveReconstruction(solveId: number | null) { - const { data, error, isLoading } = useSWRImmutable<{ - data: SolveReconstructionResponse - }>(solveId === null ? null : `${API_ROUTE}${solveId}/`, axiosClient.get) - - return { data: data?.data, isLoading, isError: error } -} diff --git a/src/components/DevResetSession.tsx b/src/components/DevResetSession.tsx index e3d510f0..23e61647 100644 --- a/src/components/DevResetSession.tsx +++ b/src/components/DevResetSession.tsx @@ -1,4 +1,5 @@ import { axiosClient } from '@/api/axios' +import { queryClient } from '@/api/reactQuery' import { useNavigate } from '@tanstack/react-router' export function DevResetSession() { @@ -6,6 +7,7 @@ export function DevResetSession() { const resetSession = async () => { try { await axiosClient.delete('/contests/round-session/') + queryClient.invalidateQueries({ queryKey: ['contestResults'] }) navigate({ to: '/contest' }) } catch (err) { alert("either you don't have any results to reset or something went wrong") diff --git a/src/integrations/reconstructor/ReconstructorProvider.tsx b/src/integrations/reconstructor/ReconstructorProvider.tsx index 50616888..f5769090 100644 --- a/src/integrations/reconstructor/ReconstructorProvider.tsx +++ b/src/integrations/reconstructor/ReconstructorProvider.tsx @@ -1,7 +1,8 @@ import { createContext, useCallback, useMemo, useState } from 'react' import { Reconstruction, ReconstructionMetadata, Reconstructor } from './Reconstructor' -import { SolveReconstructionResponse, useSolveReconstruction } from '@/api/contests' +import { SolveReconstructionResponse, solveReconstructionQuery } from '@/api/contests' import { cn, formatTimeResult } from '@/utils' +import { useQuery } from '@tanstack/react-query' type ReconstructorContextValue = { showReconstruction: (solveId: number, onClose?: () => void) => void @@ -21,7 +22,7 @@ type ReconstructorProviderProps = { children: React.ReactNode } export function ReconstructorProvider({ children }: ReconstructorProviderProps) { const [solveId, setSolveId] = useState(null) const [savedCloseCallback, setSavedCloseCallback] = useState<() => void>() - const { data } = useSolveReconstruction(solveId) + const { data } = useQuery(solveReconstructionQuery(solveId)) const content = useMemo(() => { if (!data) return null return parseReconstructionResponse(data) From 9c69588615b0321c6f8b0ad13029c1c36e9ea0c0 Mon Sep 17 00:00:00 2001 From: bohdancho Date: Sat, 23 Dec 2023 21:57:44 +0100 Subject: [PATCH 11/27] refactor: remove reload after choosing username --- src/components/PickUsernameModal.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/PickUsernameModal.tsx b/src/components/PickUsernameModal.tsx index dc533de6..62c16382 100644 --- a/src/components/PickUsernameModal.tsx +++ b/src/components/PickUsernameModal.tsx @@ -1,4 +1,5 @@ -import { putChangeUsername, userQuery } from '@/api/accounts' +import { USER_QUERY_KEY, putChangeUsername, userQuery } from '@/api/accounts' +import { queryClient } from '@/api/reactQuery' import { useQuery } from '@tanstack/react-query' import { useEffect, useState } from 'react' @@ -21,7 +22,8 @@ export function PickUsernameModal() { } await putChangeUsername(trimmedUsername) - window.location.reload() + queryClient.invalidateQueries({ queryKey: [USER_QUERY_KEY] }) + setIsVisible(false) } return ( From e3664cc642b94a15babf11190a66976c1756c39a Mon Sep 17 00:00:00 2001 From: bohdancho Date: Sat, 23 Dec 2023 23:39:39 +0100 Subject: [PATCH 12/27] refactor: migrate solveContest to react query --- src/api/contests/contestResults.ts | 3 +- src/api/contests/solveContest.ts | 95 ++++++++++++------- src/api/contests/solveReconstruction.ts | 1 - src/components/DevResetSession.tsx | 7 +- .../components/SolveContest/SolveContest.tsx | 42 ++------ 5 files changed, 76 insertions(+), 72 deletions(-) diff --git a/src/api/contests/contestResults.ts b/src/api/contests/contestResults.ts index ba7da4a3..b956c1d9 100644 --- a/src/api/contests/contestResults.ts +++ b/src/api/contests/contestResults.ts @@ -17,6 +17,7 @@ export type ContestResultsResponse = Array<{ }> }> const API_ROUTE = 'contests/contest/' +export const CONTEST_RESULTS_QUERY_KEY = 'contestResults' async function fetchContestResults(contestNumber: number, discipline: Discipline) { const res = await axiosClient.get(`${API_ROUTE}${contestNumber}/${discipline}/`) @@ -25,7 +26,7 @@ async function fetchContestResults(contestNumber: number, discipline: Discipline export const contestResultsQuery = (contestNumber: number, discipline: Discipline) => queryOptions({ - queryKey: [USER_QUERY_KEY, 'contestResults', { contestNumber, discipline }], + queryKey: [USER_QUERY_KEY, CONTEST_RESULTS_QUERY_KEY, { contestNumber, discipline }], queryFn: () => { return fetchContestResults(contestNumber, discipline) }, diff --git a/src/api/contests/solveContest.ts b/src/api/contests/solveContest.ts index 1b2b2a98..05acdfcd 100644 --- a/src/api/contests/solveContest.ts +++ b/src/api/contests/solveContest.ts @@ -1,6 +1,8 @@ import { axiosClient } from '../axios' import { Discipline, Scramble } from '@/types' -import useSWRImmutable from 'swr/immutable' +import { queryOptions, useMutation } from '@tanstack/react-query' +import { queryClient } from '../reactQuery' +import { contestResultsQuery } from '.' export type SolveContestStateResponse = { currentSolve: { @@ -16,42 +18,67 @@ type SolveSuccessful = { id: number; timeMs: number; dnf: false } type SolveDnf = { id: number; timeMs: null; dnf: true } const API_ROUTE = '/contests/solve-contest/' -export function useSolveContestState(contestNumber: number, discipline: Discipline) { - const { data, error, isLoading, mutate } = useSWRImmutable<{ data: SolveContestStateResponse }>( - `${API_ROUTE}${contestNumber}/${discipline}/`, - axiosClient.get, - ) +export const SOLVE_CONTEST_STATE_QUERY_KEY = 'solveContestState' - return { data: data?.data, isLoading, error, mutate } -} +export const solveContestStateQuery = (contestNumber: number, discipline: Discipline) => + queryOptions({ + queryKey: [SOLVE_CONTEST_STATE_QUERY_KEY, { contestNumber, discipline }], + queryFn: async () => { + const res = await axiosClient.get(`${API_ROUTE}${contestNumber}/${discipline}/`) + return res.data + }, + }) -export async function postSolveResult( - contestNumber: number, - discipline: Discipline, - scrambleId: number, - payload: { reconstruction: string; timeMs: number } | { dnf: true }, -) { - const { data } = await axiosClient.post<{ solveId: number }>( - `${API_ROUTE}${contestNumber}/${discipline}/?scramble_id=${scrambleId}`, - payload, - ) - - return data -} +export const usePostSolveResult = (contestNumber: number, discipline: Discipline) => + useMutation({ + mutationFn: async ({ + scrambleId, + result, + }: { + scrambleId: number + result: { reconstruction: string; timeMs: number; dnf: false } | { dnf: true; timeMs: null } + }) => { + const res = await axiosClient.post<{ solveId: number }>( + `${API_ROUTE}${contestNumber}/${discipline}/?scramble_id=${scrambleId}`, + result, + ) -export async function submitSolve(contestNumber: number, discipline: Discipline) { - const { data } = await axiosClient.put( - `${API_ROUTE}${contestNumber}/${discipline}/?action=submit`, - ) + const query = solveContestStateQuery(contestNumber, discipline) + const previousState = await queryClient.fetchQuery(query) + queryClient.setQueryData(query.queryKey, { + ...previousState, + currentSolve: { ...previousState.currentSolve, solve: { id: res.data.solveId, ...result } }, + }) + }, + }) - const roundFinished = 'detail' in data - return roundFinished ? { roundFinished } : { newSolvesState: data } -} +export const useSubmitSolve = (contestNumber: number, discipline: Discipline) => + useMutation({ + mutationFn: async () => { + const { data } = await axiosClient.put( + `${API_ROUTE}${contestNumber}/${discipline}/?action=submit`, + ) -export async function changeToExtra(contestNumber: number, discipline: Discipline) { - const { data } = await axiosClient.put( - `${API_ROUTE}${contestNumber}/${discipline}/?action=change_to_extra`, - ) + const roundFinished = 'detail' in data + if (roundFinished) { + queryClient.invalidateQueries(contestResultsQuery(contestNumber, discipline)) + return + } + const newSolvesState = data - return { newSolvesState: data } -} + const query = solveContestStateQuery(contestNumber, discipline) + queryClient.setQueryData(query.queryKey, newSolvesState) + }, + }) + +export const useChangeToExtra = (contestNumber: number, discipline: Discipline) => + useMutation({ + mutationFn: async () => { + const { data: newSolvesState } = await axiosClient.put( + `${API_ROUTE}${contestNumber}/${discipline}/?action=change_to_extra`, + ) + + const query = solveContestStateQuery(contestNumber, discipline) + queryClient.setQueryData(query.queryKey, newSolvesState) + }, + }) diff --git a/src/api/contests/solveReconstruction.ts b/src/api/contests/solveReconstruction.ts index 424d435e..12cf8649 100644 --- a/src/api/contests/solveReconstruction.ts +++ b/src/api/contests/solveReconstruction.ts @@ -1,4 +1,3 @@ -import useSWRImmutable from 'swr/immutable' import { axiosClient } from '../axios' import { Discipline, Scramble } from '@/types' import { queryOptions } from '@tanstack/react-query' diff --git a/src/components/DevResetSession.tsx b/src/components/DevResetSession.tsx index 23e61647..5d058865 100644 --- a/src/components/DevResetSession.tsx +++ b/src/components/DevResetSession.tsx @@ -1,13 +1,16 @@ import { axiosClient } from '@/api/axios' -import { queryClient } from '@/api/reactQuery' +import { CONTEST_RESULTS_QUERY_KEY, SOLVE_CONTEST_STATE_QUERY_KEY } from '@/api/contests' +import { useQueryClient } from '@tanstack/react-query' import { useNavigate } from '@tanstack/react-router' export function DevResetSession() { const navigate = useNavigate() + const queryClient = useQueryClient() const resetSession = async () => { try { await axiosClient.delete('/contests/round-session/') - queryClient.invalidateQueries({ queryKey: ['contestResults'] }) + queryClient.refetchQueries({ queryKey: [CONTEST_RESULTS_QUERY_KEY] }) + queryClient.refetchQueries({ queryKey: [SOLVE_CONTEST_STATE_QUERY_KEY] }) navigate({ to: '/contest' }) } catch (err) { alert("either you don't have any results to reset or something went wrong") diff --git a/src/pages/contest/components/SolveContest/SolveContest.tsx b/src/pages/contest/components/SolveContest/SolveContest.tsx index 62598d79..c064e339 100644 --- a/src/pages/contest/components/SolveContest/SolveContest.tsx +++ b/src/pages/contest/components/SolveContest/SolveContest.tsx @@ -1,5 +1,5 @@ import { Discipline } from '@/types' -import { useSolveContestState, postSolveResult, changeToExtra, submitSolve } from '@/api/contests' +import { solveContestStateQuery, usePostSolveResult, useSubmitSolve, useChangeToExtra } from '@/api/contests' import { CurrentSolve, SubmittedSolve } from './components' import { InfoBox } from '@/components' import { userQuery } from '@/api/accounts' @@ -9,7 +9,10 @@ import { useQuery } from '@tanstack/react-query' type SolveContestProps = { contestNumber: number; discipline: Discipline } export function SolveContest({ contestNumber, discipline }: SolveContestProps) { const { data: userData } = useQuery(userQuery) - const { data: state, mutate: mutateState } = useSolveContestState(contestNumber, discipline) + const { data: state } = useQuery(solveContestStateQuery(contestNumber, discipline)) + const { mutate: postSolveResult } = usePostSolveResult(contestNumber, discipline) + const { mutate: submitSolve } = useSubmitSolve(contestNumber, discipline) + const { mutate: changeToExtra } = useChangeToExtra(contestNumber, discipline) if (userData && !userData.authCompleted) { return Please finish registration first @@ -20,36 +23,7 @@ export function SolveContest({ contestNumber, discipline }: SolveContestProps) { const { currentSolve, submittedSolves } = state async function solveFinishHandler(result: CubeSolveResult) { - if (!state) return - const { solveId: newSolveId } = await postSolveResult(contestNumber, discipline, currentSolve.scramble.id, result) - - mutateState( - { - data: { - ...state, - currentSolve: { - ...currentSolve, - solve: { id: newSolveId, ...result }, - }, - }, - }, - { revalidate: false }, - ) - } - - async function takeExtraHandler() { - const { newSolvesState } = await changeToExtra(contestNumber, discipline) - mutateState({ data: newSolvesState }, { revalidate: false }) - } - - async function submitHandler() { - const { newSolvesState, roundFinished } = await submitSolve(contestNumber, discipline) - - if (roundFinished) { - window.location.reload() - return - } - mutateState({ data: newSolvesState }, { revalidate: false }) + postSolveResult({ scrambleId: currentSolve.scramble.id, result }) } return ( @@ -62,8 +36,8 @@ export function SolveContest({ contestNumber, discipline }: SolveContestProps) { className='px-3 py-2 md:h-[54px] md:py-0 md:pl-4 md:pr-3 lg:pl-7 lg:pr-4' {...currentSolve} onSolveFinish={solveFinishHandler} - onExtra={takeExtraHandler} - onSubmit={submitHandler} + onExtra={changeToExtra} + onSubmit={submitSolve} /> {submittedSolves.length === 0 ? ( You can't see results of an ongoing round until you solve all scrambles or the round ends From f499e19717618c186e4e1d9e6a361eec75204370 Mon Sep 17 00:00:00 2001 From: bohdancho Date: Sat, 23 Dec 2023 23:40:15 +0100 Subject: [PATCH 13/27] refactor: remove swr package --- bun.lockb | Bin 143834 -> 143053 bytes package.json | 1 - 2 files changed, 1 deletion(-) diff --git a/bun.lockb b/bun.lockb index 26af03a0aeb014e1419a0c2de00433ebcfaa798e..0e26bb31a6b0220e7d23b7b7e087bcf83b8e9120 100755 GIT binary patch delta 30410 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 AdH?_b delta 31257 zcmeHQd3;UR_CNb1m)wv@5J@CLVjdDQ-Vkn3Q*uo)MQV&BBxI7rtnSqsV~mYm^E}T} zQM9NQDT-FjN|l9TP z>b1wEcTP?n5MF%2qCNphU)Gd^(+95$nYXCRZzJYy-@k8Mbf7HJm7W({UJmP_J(IC@U#GAqOEldr2xTNwyxKM$kESlH?9L6|@-WL#E0n8}jl4tiKhJBu7XSGjjYB z;tlj3I}A8DN8p;nlcl9m&df?e`?Ha*6w)Plt9ryIWMrkIygrbV zeR9jG^7i0i0(ewHPF6}fA7tlhZ2=jzVNh~PLNfBCWG4+qkHta_F99X5*an8& zQ-D$hRX|}B+bv{5|Jr2mXoGEf4K;sV3=gU(J1K2oQWh*)li#whV)d(~7WX+QMZ5Z| ze6Otdgd`Yd9C)RDpisqDAwZ4SeK0X4OY!2fn%o`bkiqlO4A|Z_4AsIdZEfmEk}GH^ zD57jtK~Y{!n+F19Fgs8;(5;xSs3Hlo4|z{-6hQK^L43cViX0HaZy9{7;i0NE0zj#G z>GA0qDTxw%vDbi<)I{l?Nu6EJgHkC6LCMFqY4SxHKUs?(uE~=%c{@;Qwprt=g1Uou zRpnOOvoN*bTcA|%8BmOW+X0PV4_XHN98hY&7*J9p3zX7#1uX;GLgU8=tNm{UC50R# zRXer^CH^2N*>5#yIhuu%va&L={F4UnZm1@lr6u^Qk)V&VzZwxyjnoEod`ptx`!ir<6p4I2CHsIluw#HGCR0Xs$ zD5e0LBPgZss-?f#LXv#JKWM79sBd;sb^?Z6$(E|e9|1*rt8E(s)G5DNRC=tHYU4&I zh+N1Iv^?n5)@lU@L21k}$f<&qnyQLJ;!{&ovU8+nZB<1Y#Hf9PYU5Mm6O-zIr-plj zQs23N7NpO6Lqn$ikW|HGf-eX=X|m z)`4s(Z7A%At4bF&T~2aRI?Z0r8t(~8<>)Jc8TqO6vy!qiQU@kUKX+9vc^i}(b`jJ{ zCdNFQF(@62ZFjYR!*Oa?oB~hr3E67r?Sh>6m7t`7vLK>C>ENlN`Jhzryq;<~Q?>L> z<5hVkwH8+(c zYEbP&bzH@#4@ry9!Qjd2qvY!l$Fm#*taFo9AI<|M1Mfm4&8}8dLha7$t1c6RG#U@8 z3~NwNDqf@Rpp;JnC6~Q}{G`WMpd`-+bp-tgl*ZB=)KdXoYnu$?Qi9SMs$pbMa<%^H zyk0RM$B~)p7|G1!y^2+7M>{)84HGLvOV6z@x%v{VFAh1{5}t!SQ!nKC^CFU8oS!Qe z;BaAR|KR~A7FOpj#XUT>H1Vmr@}%|Lq4c+BwK-aK=rE6nUsJ|*=)I*{@iX>Y99%~g zH3XmuGIzC@4978x2lGiD5poH8?&xVYCfiF=Jw)5{6D>`~#o#)DV?58pWOOq~5ICt`jFziGqj6(7gr0_BuLmkw5&_pkj(F(4i(gtvbFTl0nN4+A9?nX(f4>?-u zX)<&NXXZyeBjmY89$n6CG@!eJlx!!;A{ty1KB;VkaWO(K6qJ8<6UkX}yQv4YuH7t<&;1%~l3$pcF8 zTnKk*LW5$hM?XmfU!5WwW6^W==DoRLXB7-0mymbROYNZ|3EMN8Dxz)|a01WyT z5GtlH3v+)y6h?+4n zm>18lX_if1+_9F~_>Py_b`)0GWZa^0$etW-52ikMsuCgB_U4ZMW;xZHM}vIm&2vHA z%kg}W=yKdKz-;7Li&e*Utzt4B1&38l9j9_RAD-XREO+$bj6^8XZpLV=~SFhrNY1 zSgu!52EP|}22?cqx0cB`6kKa?s=W?radwIoe5<_}m9Tg`xW;;Y#K(KM;LxUsF@N9t076}eOihz93mjF0;fN-z1xLe5?Xqvdg@P-l46B;(AUFzKpc_K!*g(%1^T25h^*0$$ zfupWOuZ6<7u$jVK5FBCbfY6JQ19AmBFtGB|8IlqL?)0o*5>(Uv!N#zpeTOS9AR9A&=QlR9{oMFgBzwf8-ikCfg{9Tr zr+INKICVszTi6M1Y!WHEgz@MmW&=$`O?j&(5r(D+HCI9t5sKoin%B2~5m^?MLWE=q z2sP(UE$Z7Npys^)A~GDNZpB+QteR~AdB7a3l zEh!SMjN(qs>$Cbix_Kn-xy>W(8%UBxDWwlWQG8O9`XoXiAa&D1ompbs6a+of_kvZTY9wv5%8!eFrKXha-#kWQw1ifkcfCnA~M;@c}oWalt zeaxM(pNVV4qg$Gd6G)&IRm;S_;D=jA8jGS8ZPfxoO~zPoQ~<`PABH$MY9yQl&VCv} zwO`;QhEi`KPAPoQTRgh8*?0*8^4%iJ@b*AGG=(Y}v8CMDCepYJ0vZQM0iU}HPMvaT z-f?Tr^V^t>?V3wcOC(We-ud7#gghdQ2KZxvJO*56k{ebb)KE#09Hknf7(Y?oWY`7{ zi-|`Bd%zFJM9Mv)xnnzQL!yxp#bfMYRW4=Wx$Vq`4iKN9tlFqvgIecl0nD`m~m$p4_QTgy9)N zXmGm-LnqACUHBw~HX)QmA!9q(pDc$S#h7@ z`Sd0@*qm(D$t2fm$8%%N@`!dkKh|vc6fjD0Djq^G{!2yP%a2W6_M z(m|4jD;&AwVsL$5#1+RvP{0iYH{wOyPldQ}hfa7l^&;w9a0S^~#;VSv?jpv4d%GYd zI8sZUG|HlbsF|<^X3<&Tu=rw&dmbSSJ@WKWlUyc_M|U;LMc?MRAYI<(`CZM%wJ4KJ zXRmBdz6FOF8TN_6DiYX*=XNt2hjr1dOFp;>9JxCj91HV(aOCc22X=aaU3qSIvoXG_ z+Bu4lE$7A_k;c;9B&oNOjkY~QyYbu}X7&L;jHurbMLw=Lu(5u3wM4}&j3dB7Ra$R4 z!DYbFU_>0Y@_{{&k5(8%5<=LWV+*$mAvg}&L(}3TaCMZDU2B^RReRFVpcN?|AsUuw zAl8%B;7A8$3v0Xst|_<@$}H0+ULU};-c10fl?UB-frH~wDIO+6v0ku?(wZoQnke;5 zLWsOnS!mcvK0YzhXqTWq8-V>V-3NlxCWru&oR+|&lg!2~5K`yDXSBV}WM9-VHMTleR=>1IQ2e@Tj?<)2;U#*9dVcNR@QPK_cAQxIySgf1ZzMRpVvm&e&B}pR405+Hs1FC4WDkxnf07@_e*DcvZICo1!& z*(NJBFISVkic)D4v~-0j$tP;^S5ZngNlQnR_{n5vYQR)2f+$7gX>^)KIVfF3DSkRY z_7WQZJ}9X-AE1jUrCUG@uELb+Tck*da>oHBavN>wh`c%l@)LgR^2 z(W?NGuLkHM%6PW{?opI`198AcfG(oMZz2X)VM?WK0VsYeKo?QQ*9~yD+9UKKalkHs zu2)eiZ4W@{KLY3?O7gt`$@c+ty^K=p4*-MhvdP)Bye#Ko#5p=pst_?`rg( zM(=~tMU>)y(C7nDx?WJyp9=gDZ~%S-=z0~UE_)78!Rk$=RF13&RZ6@)cuMa8S`4%l zC?zRPH(dP8pyE~vyWvKCJxQnQZ=_^{zqP=BDUZ4?7!4;HuF(ddWSK@x ztsem;pF%0vSmTM3DVuR13QOiIONvi;vdgiBi5+8ef=_vv(rtQA(&d#-Au{&oFeMk?3pvdi2etUZl+?|KoT5Hw*fCJxh?cN0C3~FILC`?II47cK-?pplcMwMNP zREkK&b5QFPwvQaWXip z-=Lv!R}X)l7x|Xm#tL80`r_!ysbgmB%FgMMwR+3P^-t~k_VGWK_vOa&YLlF}|3nLO;zK6J@-yHrf-AwRPm1N^COh#- zlPt`Yp9L2_#fdkVY+qyH;P!y4 z$c@uudGZV=o;KaWD)XJ-JZ3uaax*NfDo>da%MXD&3a&c$oEggp3n%{0Obe^Y4}+`r zKJ*h7=Ff)+=m+j1xZ1q>`_OL|^n2gJ0{K~R;j^LNEDHmO#J77S@UnSq%NaT?E&LS6>4CmO{TJ78b+Lf(u^;{gzr-dp>?C^aFPnTt{Aa z8T4BY{gzo+EWZh^#R}-R+`{7ctmV)T+*5E}c+?8$w-WlTu&{3Y3AoOypx;Uh>%mv8 zgnp}`-zp1>=W(l`AGkf>61Z_S^jib{R$EvS-wDoRE%aMsVSRYY8t4b^D7X~vxfc4Z zgMMo*tRFuNuG)I&x6Z=S_>gtb58Op?8NB*>=(hp-t+%lL{4BWejnHp{g=O>c8=xP! zyWj@!x*MV2Cg``(!Upl1;96{kew!?82%ohH`hj~2ZWxc+4E?r1zs(l*4u1l!^H%7$ z#loz7)fVWt4f<`hu#r4&EA#`m2i$wyxDEP!0R6UE*l4~JoX2+P_ko3tMheFXjXSlB%N z1YGBjq2EUqwt%nt2>R`Xeji)dA|CfK^aHmC+!Aiw3;p&%zr7Z=jPC^Ju^;;Fv#=F> z-#+LEuJV2hTgChDhkl6C?C;hVti0O$6#1^=jw z{~CRC+KGP(?izPJjXpZ##0Q+V;ENdhz#Rft`HY3#@Gh6?hLrla~6DiY2-QRcOLqId%y$FL%$2q@4SWm$ghCA0j}u<3;UVpU4VWUp&z(M zywOGIcM1Akw6G`q0l3HDI$pA{-}s_S(C-`Q_l<=;EP8_$?F!XW*{iLcyz0@LLNj%J+dg1g`Q`3p4WmSE1l{P!L=( z?)x1SyaoloqebonxHI5FuhAkm@){Jp4h6xv^1$m*@OvnDoff$(;BJ6x`aLajdEZ09 z8&D9OJ8yIY3f_c*H)xT20PZokjyGwMTXYi&-hzU+XpxJ#1qE+IL2%_cyA1{JK*8Iz z$ZZ0*1Dx9(TIAyIK)<`t4_syLdKdcLgMN2uk=qCE5V*?sXp!rG5BlAQe&A|y-}}(- z2k3X77P%AP&VUR3ffl)uKR~|+&<|W74}1Xq9zwqdw8&incLQA0hqTD$J%oNgLO*aO z-snf@_Y?H{krufJ;2wkP_!BL1i++NBKSRHtX_1Th8T$PK{lGLySe&CvN*T>NB3G{nRi`>4)v8)C6d=iT#u0QV4{4nko?)z&jYsrV; z-in{Vy*01?TPzm0k+`?zXK|0=fxpMHc6>bU?fI48q0>|7^pqC1yrFfc4^Gh$e*=*+-w1KUF|2HrttP9ljx5HB{7 zUUZW~>1P-eCL zk1E#cRi8z?Nsa%gF#XFJPxt>Qe1o#?uh;zIm;)a9`}!++MIF}Hs@*Fp*6M}+ zZ_3m!ZH)DQYyR@k|MfZ7OEvzZ!ixRT7k`%%V)0cjqWZ|8(WJi$UkzV~)E6_7S?T@w zKMUWW%=(AcyupgKdSU;+GR+%ljP-wOeyLSqXZTMvCg&QMl9NPUw_a>2#Y(Y15V!Tg z>g&XmFe@W96+hx3Z4s` z&r*L>;WbtAibh-iR~2jZT3?S@U!n0I6;@`#*WAaiYK&F+F^xCh@46MB|7-B-S84o5 zg)#d6WjN8W`}6cjvcB0F=@^;PS+UO@_G27>pUnCuDtSd6)>mjpR?vSHME_V#GW{1qbQyKie=1YP_{VcM{D4>ve()yiv1n8tC9A~x zxVQSjgzBbOA?PVvP;$YCmDem&d*wA(qV&qk>%c_uhBf|UW2|q^)PJm4t5=yQUv8Q= zVj1iI*8Ith|9elze+ues-|NSQH{}(^hx4}XH5>=`rF{q9K}r?Pl39K0v*+qXvJQx% zSo+E~T{=$#&{vV*CoQxzg}(>h2{QVyP4J||xZc*%(RVPrC=yUjMlTbrMHrV=>Z%E; z(haIm>ZZx)yG*+@S$9oF-!rcaP(3{~8U1slDFoE0o|=qa4{EN-;x!q@lhi_!^-^ST zGJN`z_Jg4-K}$&A_ouHP(v_&m=$)4KR4Xoer-h>N+(ep50$jZ{nJdDv6p5>kCM$_> zoF=39T&S2*KzHR83IwQEC?1bEU%aW)Pm{Sp_7=iaa4INiNiTLxMVJcC(9*dhY(bdH z$kft#AWU!iP#M`;I!}a~X|i7Q6+`^dj~^-tsgMDh%u7p1GJ4B~(s={)st^@CNR#35 zsUo8?25T}YjZ#a}cUj_ICi4^4WM&Y?er&AO57LG(0KH8>Zy1CF^aes0Pzssot6RZ9 z2tcn&cmU*8UVt}H4)6iW0~G*YfL^(v*D&k=d%yq`0g3{#s4ot{d(GsDqI@bdux`RP zm6a0Vsm$Bj7ZLq{R3HsV2l@keEFonBIlus5ATS6R3=9Q^0mA`$*UTND7jM=8e*az$ab_3gi9l%at6R;Ur3#8o4_658e}(s+rYQr=>?(-0KIs02B6oIz68Dk_5%9=8sDD) z2Y^q3&qPc*vsh~&SQDrP_ya3|U?2nt1;T(jKp=1i)wKj#0j+^10KNTT0%`&@HCzG} zNP7(Q3*cj55AY$d71#o71U3NEshAlE(EKqK7zd08?f~>|6216z3ZN;%28;xb07rpi zz;WONK<{%M0uGC`3|8Ka=1!U`djLHFn&%S4q6}8b8iQavpgqt5s0Ty<%YbiC>t*0O zpe@h_zz0>SF>oB%3v2@x0eQe(;A`Zgm+KZHoDbR<7!FJVCIh2^F~C@0969=U1o{D~ zKoZagFawc51PYl3+yfecFAICpJWum_Gl1rDn!EjhPawa6G~a`s2dx4c2YG9tCGZ7g zG;Py+M*?v)droEOy*vc^0~Ua$#il?rVA`Mf7T}u$I;EE&2`<^V;Tk+#nboXR2^p9S zO$J|&xGtcy`gH^-Kh;4Er242KG)Tx&q+}UjGI$y=Yk@VuDqs;n%F}G?1-Jku0B67n zC;}9NZWH>ma#lw$v?`LYDBw^iT)a@2GSl2oYez|d))QL%OKIWK8gD0&O_sA zs;7A5u_^&-X(hl1p!J|4;0u%oDgb`KLSONx{Hz`j4$#=G1%v@XKp;>9 zs0LI8ssjN)9iTSg4^VmunbrVECCh++J5-C5n+nc`IQK-*7zcz#eh-_Q?VBUM}c*~2f$`v z6R;5wCk8W*a_bS=ph>oYZUwdgM}W^ow*joZ^$LQQ0aujhLZu?C%f3PQ3!o&RoAx-u zqz#$q6!0Z*5;y^T1zZHa2F?Lzfz!Yl;5={vxCBtV15kpdW@l9D1QZ8o-$DBiBj5mB zL879dRM7+Aci;)|7`P4G2W|q_folLc$alb1;9KAZKox!u=ws4Z9L!UI zTKx>Lhs+L;0SRCL^&X|u8*l~TSAi-hiF|HAX@DG){G%blwD8d?Y^{qMy~;lW5sd-z zRug#Ify_jhb|bVap~}g@Xjc*d(C&qHF;xLyfcBx(unM5%0rH2kpq_vSP!8|`yZ~>F zZ%1P|1_|2&=!q9IPbDO%2>1b&fhs_Cpc+6GQbTClMB67CCdAj)&>mk7Ts^)AozDVO;_Ax>YHJ=9_p37J*WO=ptDHWGeUkd6E_`TyYv?v|maF zdIMBBrSAdSM~kC96*XcSKt`oBDUkIAomr#l zYXgakaNWw>Dm_N992TU%&OX^7rjB4ivYownO!RsYQOn9ay!9h1TDMBEpKL!mR?)6b zKrrTrBBH02HIWTP#d0f4m+u!9Wo)daob4bIZLGKav4c2bLrLw8Ge@$)m2O~-peE=? zQh52`{_PEethaorMfZ;3kXzfd#kueeHXRJPNM5( z&?-)1;k&GfV^b$(=ZG@{#684^Z3FL#_FfrxK4(+we0+P*&WP z6^cm$$t0JYME&<*H2pw>2CXxPlpDRjK2o5^0|Nqs?VLp>rSNrDYx}s?oO2N!J)#xe zf=IVOXR-7>n6ZVkcu2IpvuHR9IrN{Yo-?b`N3HmZpOC`@ZwbOy#91ty!ra98Q7lFt z<18*9xje&J{4xsGU*;@oj0WB4EV>ij=`5xZJ>V=pBYM(V{7UqZvj`Z2e0C+o@yV!8 zJ<$V4EVyjiHFQe)`$_{t$h2N1#Bika*3S+p=Xa&zUZdYmqzF?TyLJh&ky7ZVioDI{ zo^SSO0bH^>`k71^>>_MqnTPm&3@hucA11P+;eulpY4^`cRY7FWPA(#7EIgs7i)yXG zJAXDA>P4+V3iM?_*o%U^^#e-6XZla~{iNh!ctV{3rMX&Fa?g@8NiY2%lGvE9T6KGP z_=-}Q;xY$H3V9qV(~lf+oOk8isG?VcmCDd}SSQMewv<9YOQciG=dI&iwjM+Z(j2zA zT1LzthfX`=F7AwjHB&r9k@3tXm+}<1Rzj{?*G=3W&&tU@o+5P>jNq5cT6^mUiKJEc z>9u-#uQoCZR!xPYL&U^f^iYDg*qe(o)$aBX4|7>b$Dh2_-&oHrCysA}disx;_iHeH zT>mdGP}4%yIR(Wuo`7Nbz(=f}z*>eKD6f98$MI%txyXeRe?>~F5lXyMLD{g`TG!sc z*fal=(ufHOQ0AC%6-2FxXi8gO(Q_ik$O2#W-##ZguRb32(*lfTjK4rkeJ{>I@jR4g zyf}Iz^;j@sV8qgXVmf%t$wL2?` z5tE>setbwo`4-)WPK&mnOqwq-0wSx3&6ChE`sp*}?SprT1scU8;(? zlc9j-a=Q_)!WTH+(3eznBIsjPR{zgkKe4Y0#s%qkd3m(uyP(0GhE>Ys7`!etuNJLa!`lg7StP=}*MR^#jW`rxe)Aeu}=S!V-8o!RKg zKdR#)4opMi?dpijC?-rl!ld%@Z`>Vc51`2sGXM>4?UWN+K65?SX3Y(9K**^Hp>;$6 zXC9^Xb5TZ>@*B7Dv!gd<)~Jqf-pcBE|Mk-8QAcd#tfhE+1R60mNCZuX#q_fwJ~`dw z&dBeFQD@Rb1i#Ra6cOKD8+h)*go%g=SABhausAlI)nK#4lj&?cyD8qA!RFwI6;WX( zyfz?Iq|bzU`dKSAPc6wh(Qk8CMTt7(lltK$hl;ip&2FUaw`b5hJRqo!)HO`(MqY3I zxD$_Ge+_JxAC^>*VoaF$0VzWD15)ZVp50@6krHx23jGKbC-){d=dT}lups7uXdsxU zmwx6-%Fg;LLyDwbFGz4p^hW~rOiTdr)(&m4t!b9hEi1C>+=6sfO=7Qriuy4xHA+?s zW)rS1DoCLp1e4lx@$;|qb~Y-ANj3@R_o0A(bj{={AE``3jJ`IarSGId$`sc zToAKJba@{N=%>-_yOy`Ilza0>1qlv{DM%2eA7A6(YOcA1O+QkQKtJFntmTmp7ERka zv>;}jS@fL6JiPRSTT%`mEs}Y%=SG=ny)G>iuCrjBeWE6a_n+Ovh1JkDe_Bale_ay8 zZW((X+gQW2YUd-)pNDINsZ+6Iq*#wu$m$Nt&4Bq56Vtb|FfR2YZIVNKEU#Vb?hM6K zm3@mk+JfMZ`{uLKV$f_>-b+33MzTfZF1`@t`(z(d()JM3nhcKgcESy}>?_zpe8p+ba>$yG?}8T=b59 z^viD_jY{uW`ogw?6sJYZTR*w0`|j&O zJI);bsvreAaPLy)!7hlqC{i`6n*qiMejt43p%T_i)SrhU{F*5b=4^38m+tx5?b3QE zj*V9^rm#lML?%+mZJLSh3s@PSU`aowr*WH(ZCXv@AE{|kXlTD?!nTN&u@9DpHWQZ@ zu+l}SgfY#;!+ER;dtaF5vj}hfbfC(gE^lAetFOIYRfwe7)=NJ}s8Q)PDP=cw8io|K zOrjSQzwl&AkkIM{`SD8M7mQlHQb{I*w7oWdR1Ze#TI}ofA6z=9aiwPZ26L zj#56K*-ET=wsyemO3r#rn53%eA!|yAsSBCCn7V+4Xc9ff!~3sIq2)D@qbjX-t3YPg zk3cH%R;L z=pxKjS6iz)fs7Hn>Dd)Ej_SI-%G6qy%c`T6!6!J?4tm{Xd;&xD!<9bsF7<5Q%@Q7Z zxpk##ZN>b>&_w@+R>u2Zlo&Ctr9g_1fS|yDx;W%aoLtOWwpCAT!r6Y)5{B-Zx9}aA z*~O?sT|d+5c0_#q;h@$=3{gfssoq4}Z_KEOKJZl0#uPRdP8} zrpJk%%Q2Gl1LAI{9hlSk`k|sOMp863?!;5sA(_rGVbfpYsypE0&>ycMCl(@Vc z8hfhSPiLXGX=Xsd&q2^uXOnpndo{EFxAj zPceK23(-f2`E7Az1q;HLkZ4g59xE|io4+kuuSE6EUBq!vZ~c_3cH_TZ@6$Qjv7p8( zUBuj#FjEGei4=z+wH$>%(}b@=k82-iyy*NZe7dzQ={s=1kY`=Wp(GmKu-k31U=^m? zpT*8qELg7HUED)Hc1rl7!Axm)Fqt%SHS5jhiPNi*C@@|WUxQ{gjTcqcu-RVAP;xO8 zK7Y)vw^lxNUXD*3p%~=o&1K^J8e|w0CtTK|YtXsf*P?6mGprIPe$Xl9!u)$mYm^T7 z>#jlGwaDu=B}w_8jqUd`a>tdu4y2cUvHC9&->k*3-j4+M0A{;7YS0+3k(M6|5?n$| zWyD-ei@P)Cz54M5F?PL0z&bR{qqq8eA@F|Sky`sUzgv(Z5HViJYkw|v@MqsGNG*tI z*;@=pUhjCMsE8DYX8HLZ`0+}Yf)t~Ai;YMTx>PIU>tNfJNqd`*DoF7$V(MB$9@fWw)E{2|^T<^^0y9TooqwG%MkT8=hJF-S^S#~0Ft?gnN}gc) zpspVl_D)bh`TeEr{18K*1JH3aOdQdm?C~w0ZCzGL44cV{QNQ0<52v!G2=5JWl8;22 z4a_rCKML$pXv~mqW#?%RR;UF1u&@VFt-Gx*9{HX*5)cH0-4hn&8y0dWh9OUF=4+-ufA4 zMOzOWlv({=@R#I;2Br0uT6}&SumH(aE_DuY#S?T*jK9gUJ!I7Dw=N3%U`Z<5*?(*)h?mOVY zfxbtVGsOBWth=m!YV05n$`XlNF*l&X&0A5S_NY%*l<^4DkCB@{aJPTTNdq~ri9V19 zz2s?PXh4{KRZElLs(|U?N&f?u|c)s{=JgK3lEa-NvOnNfu z^8x$JadM9Gbe$d}D0O9H4lD(4{C@qAyx4F1KSDdTb?J{eJsdqgRG(a*`A~G<&a!+j z!!k65j|vYx!Uj7(%ZX1(9fX5&n|C=`PW)D_`$&oI2N@?>KB} z?3xOmx4~2Hk?tLxpO+4~^a#8Q_)qKhXdgMS-f4$jYHf`JxA)z_me~2=$#`x5+PM2G z55xm<`V7Tasc~SVe@1%h5Rrb9xjS?Z#4}aw(!{L~S)k~)n|TyZ&B(}3@=r=1h;t3a zm4nPnOx?}6=x~!|dlt%%q6=}Ne?F@$oOZJcqV8^1X?w*z?0HQQdx?3^*ee&=e(WZD H-sJxPEeeYj diff --git a/package.json b/package.json index 7ff8190c..1d239862 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,6 @@ "jwt-decode": "^3.1.2", "react": "^18.2.0", "react-dom": "^18.2.0", - "swr": "^2.2.4", "tailwind-merge": "^2.0.0", "usehooks-ts": "^2.9.1" }, From 5e6cc9a9a2b510f142f076e6622035ab718beb42 Mon Sep 17 00:00:00 2001 From: bohdancho Date: Sun, 24 Dec 2023 10:22:48 +0100 Subject: [PATCH 14/27] fix: reactQuery adjust default retry option --- src/api/reactQuery.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/api/reactQuery.ts b/src/api/reactQuery.ts index 843f5e75..c8fc84bf 100644 --- a/src/api/reactQuery.ts +++ b/src/api/reactQuery.ts @@ -7,6 +7,7 @@ export const queryClient = new QueryClient({ refetchOnWindowFocus: false, refetchOnMount: false, staleTime: Infinity, + retry: (_, err) => err.response?.status !== 403 && err.response?.status !== 401 && err.response?.status !== 404, }, }, }) From c17f32907662f8af12499df956481f6a471ca827 Mon Sep 17 00:00:00 2001 From: bohdancho Date: Sun, 24 Dec 2023 11:26:30 +0100 Subject: [PATCH 15/27] refactor: migrate to features folder --- src/App.tsx | 6 +- src/api/accounts/putChangeUsername.ts | 8 -- src/api/contests/index.ts | 6 -- src/api/contests/ongoingContestNumber.ts | 13 --- src/api/contests/solveContest.ts | 84 ------------------- src/api/contests/solveReconstruction.ts | 25 ------ src/components/DevResetSession.tsx | 5 +- src/components/PickUsernameModal.tsx | 4 +- .../layout/components/LoginSection.tsx | 3 +- src/components/layout/components/Navbar.tsx | 2 +- .../user.ts => features/auth/api/getUser.ts} | 9 +- .../accounts => features/auth/api}/index.ts | 2 +- .../auth/api}/postLogin.ts | 5 +- src/features/auth/api/putChangeUsername.ts | 7 ++ .../auth/api}/refreshAccessToken.ts | 3 +- src/{api => features/auth}/authTokens.ts | 0 src/{api/auth.ts => features/auth/index.ts} | 8 +- .../contests/api/getContestResults.ts} | 13 ++- .../contests/api/getOngoingContestNumber.ts | 12 +++ src/features/contests/api/index.ts | 2 + .../components}/ContestDiscipline.tsx | 12 +-- .../contests}/components/PublishedSession.tsx | 2 +- src/features/contests/components/index.ts | 1 + src/features/contests/index.ts | 2 + .../contests/routes/index.tsx} | 6 +- src/features/cube/components/AbortPrompt.tsx | 21 +++++ .../cube/components}/Cube.tsx | 0 .../cube/components}/CubeProvider.tsx | 65 +------------- .../cube/components/DeviceWarningModal.tsx | 40 +++++++++ src/features/cube/index.ts | 9 ++ .../dashboard/api/getDashboard.ts} | 12 ++- src/features/dashboard/api/index.ts | 1 + .../dashboard/components/BestSolves.tsx | 6 +- .../dashboard/components/ContestsList.tsx | 4 +- src/features/dashboard/components/index.ts | 2 + src/features/dashboard/index.ts | 1 + .../dashboard/routes/Dashboard.tsx} | 16 ++-- src/features/dashboard/routes/index.ts | 14 ++++ .../leaderboard/api/getLeaderboard.ts} | 13 ++- src/features/leaderboard/api/index.ts | 1 + .../components/LeaderboardDiscipline.tsx | 22 +++++ .../components/LeaderboardResult.tsx} | 31 ++----- src/features/leaderboard/components/index.ts | 1 + src/features/leaderboard/index.ts | 1 + .../leaderboard/routes/index.tsx} | 16 ++-- .../reconstructor/api/getReconstruction.ts | 24 ++++++ src/features/reconstructor/api/index.ts | 1 + .../components}/Reconstructor.tsx | 0 .../components}/ReconstructorProvider.tsx | 13 ++- .../reconstructor/components}/index.ts | 2 +- src/features/reconstructor/index.ts | 7 ++ .../solveContest}/SolveContest.tsx | 4 +- .../solveContest/api/changeToExtra.ts | 19 +++++ src/features/solveContest/api/constants.ts | 1 + .../solveContest/api/getSolveContestState.ts | 16 ++++ src/features/solveContest/api/index.ts | 4 + .../solveContest/api/postSolveResult.ts | 29 +++++++ src/features/solveContest/api/submitSolve.ts | 27 ++++++ .../solveContest}/components/CurrentSolve.tsx | 9 +- .../components/SubmittedSolve.tsx | 2 +- .../solveContest}/components/index.ts | 0 src/features/solveContest/index.ts | 1 + src/features/solveContest/types/index.ts | 14 ++++ src/integrations/cube/index.ts | 3 - src/integrations/cube/useCube.tsx | 6 -- .../reconstructor/useReconstructor.tsx | 6 -- src/{api => lib}/axios.ts | 3 +- src/{api => lib}/reactQuery.ts | 0 .../contest/components/SolveContest/index.ts | 1 - src/pages/contest/index.ts | 1 - src/pages/dashboard/index.ts | 1 - src/pages/index.ts | 3 - src/pages/leaderboard/index.ts | 1 - src/router.tsx | 15 ++-- 74 files changed, 379 insertions(+), 350 deletions(-) delete mode 100644 src/api/accounts/putChangeUsername.ts delete mode 100644 src/api/contests/index.ts delete mode 100644 src/api/contests/ongoingContestNumber.ts delete mode 100644 src/api/contests/solveContest.ts delete mode 100644 src/api/contests/solveReconstruction.ts rename src/{api/accounts/user.ts => features/auth/api/getUser.ts} (60%) rename src/{api/accounts => features/auth/api}/index.ts (79%) rename src/{api/accounts => features/auth/api}/postLogin.ts (52%) create mode 100644 src/features/auth/api/putChangeUsername.ts rename src/{api/accounts => features/auth/api}/refreshAccessToken.ts (91%) rename src/{api => features/auth}/authTokens.ts (100%) rename src/{api/auth.ts => features/auth/index.ts} (69%) rename src/{api/contests/contestResults.ts => features/contests/api/getContestResults.ts} (61%) create mode 100644 src/features/contests/api/getOngoingContestNumber.ts create mode 100644 src/features/contests/api/index.ts rename src/{pages/contest => features/contests/components}/ContestDiscipline.tsx (78%) rename src/{pages/contest => features/contests}/components/PublishedSession.tsx (97%) create mode 100644 src/features/contests/components/index.ts create mode 100644 src/features/contests/index.ts rename src/{pages/contest/contestsRoute.tsx => features/contests/routes/index.tsx} (90%) create mode 100644 src/features/cube/components/AbortPrompt.tsx rename src/{integrations/cube => features/cube/components}/Cube.tsx (100%) rename src/{integrations/cube => features/cube/components}/CubeProvider.tsx (63%) create mode 100644 src/features/cube/components/DeviceWarningModal.tsx create mode 100644 src/features/cube/index.ts rename src/{api/contests/dashboard.ts => features/dashboard/api/getDashboard.ts} (65%) create mode 100644 src/features/dashboard/api/index.ts rename src/{pages => features}/dashboard/components/BestSolves.tsx (87%) rename src/{pages => features}/dashboard/components/ContestsList.tsx (89%) create mode 100644 src/features/dashboard/components/index.ts create mode 100644 src/features/dashboard/index.ts rename src/{pages/dashboard/DashboardPage.tsx => features/dashboard/routes/Dashboard.tsx} (53%) create mode 100644 src/features/dashboard/routes/index.ts rename src/{api/contests/leaderboard.ts => features/leaderboard/api/getLeaderboard.ts} (60%) create mode 100644 src/features/leaderboard/api/index.ts create mode 100644 src/features/leaderboard/components/LeaderboardDiscipline.tsx rename src/{pages/leaderboard/LeaderboardDiscipline.tsx => features/leaderboard/components/LeaderboardResult.tsx} (60%) create mode 100644 src/features/leaderboard/components/index.ts create mode 100644 src/features/leaderboard/index.ts rename src/{pages/leaderboard/leaderboardRoute.tsx => features/leaderboard/routes/index.tsx} (67%) create mode 100644 src/features/reconstructor/api/getReconstruction.ts create mode 100644 src/features/reconstructor/api/index.ts rename src/{integrations/reconstructor => features/reconstructor/components}/Reconstructor.tsx (100%) rename src/{integrations/reconstructor => features/reconstructor/components}/ReconstructorProvider.tsx (86%) rename src/{integrations/reconstructor => features/reconstructor/components}/index.ts (53%) create mode 100644 src/features/reconstructor/index.ts rename src/{pages/contest/components/SolveContest => features/solveContest}/SolveContest.tsx (95%) create mode 100644 src/features/solveContest/api/changeToExtra.ts create mode 100644 src/features/solveContest/api/constants.ts create mode 100644 src/features/solveContest/api/getSolveContestState.ts create mode 100644 src/features/solveContest/api/index.ts create mode 100644 src/features/solveContest/api/postSolveResult.ts create mode 100644 src/features/solveContest/api/submitSolve.ts rename src/{pages/contest/components/SolveContest => features/solveContest}/components/CurrentSolve.tsx (88%) rename src/{pages/contest/components/SolveContest => features/solveContest}/components/SubmittedSolve.tsx (94%) rename src/{pages/contest/components/SolveContest => features/solveContest}/components/index.ts (100%) create mode 100644 src/features/solveContest/index.ts create mode 100644 src/features/solveContest/types/index.ts delete mode 100644 src/integrations/cube/index.ts delete mode 100644 src/integrations/cube/useCube.tsx delete mode 100644 src/integrations/reconstructor/useReconstructor.tsx rename src/{api => lib}/axios.ts (82%) rename src/{api => lib}/reactQuery.ts (100%) delete mode 100644 src/pages/contest/components/SolveContest/index.ts delete mode 100644 src/pages/contest/index.ts delete mode 100644 src/pages/dashboard/index.ts delete mode 100644 src/pages/index.ts delete mode 100644 src/pages/leaderboard/index.ts diff --git a/src/App.tsx b/src/App.tsx index b9160d78..5aec1326 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,11 +1,11 @@ import { GoogleOAuthProvider } from '@react-oauth/google' import './App.tw.css' import { RouterProvider } from '@tanstack/react-router' -import { CubeProvider } from './integrations/cube' -import { ReconstructorProvider } from './integrations/reconstructor' import { router } from './router' import { QueryClientProvider } from '@tanstack/react-query' -import { queryClient } from './api/reactQuery' +import { ReconstructorProvider } from './features/reconstructor' +import { queryClient } from './lib/reactQuery' +import { CubeProvider } from './features/cube' export function App() { return ( diff --git a/src/api/accounts/putChangeUsername.ts b/src/api/accounts/putChangeUsername.ts deleted file mode 100644 index 62f10791..00000000 --- a/src/api/accounts/putChangeUsername.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { axiosClient } from '../axios' - -const API_ROUTE = 'accounts/change_username/' -export function putChangeUsername(username: string) { - return axiosClient.put(API_ROUTE, { - username, - }) -} diff --git a/src/api/contests/index.ts b/src/api/contests/index.ts deleted file mode 100644 index 9706ebbe..00000000 --- a/src/api/contests/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export * from './dashboard' -export * from './contestResults' -export * from './ongoingContestNumber' -export * from './solveReconstruction' -export * from './solveContest' -export * from './leaderboard' diff --git a/src/api/contests/ongoingContestNumber.ts b/src/api/contests/ongoingContestNumber.ts deleted file mode 100644 index 3803dc96..00000000 --- a/src/api/contests/ongoingContestNumber.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { axiosClient } from '../axios' -import { queryOptions } from '@tanstack/react-query' - -const API_ROUTE = '/contests/ongoing-contest-number/' -async function fetchOngoingContestNumber() { - const res = await axiosClient.get(API_ROUTE) - return res.data -} - -export const ongoingContestNumberQuery = queryOptions({ - queryKey: ['ongoing-contest-number'], - queryFn: () => fetchOngoingContestNumber(), -}) diff --git a/src/api/contests/solveContest.ts b/src/api/contests/solveContest.ts deleted file mode 100644 index 05acdfcd..00000000 --- a/src/api/contests/solveContest.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { axiosClient } from '../axios' -import { Discipline, Scramble } from '@/types' -import { queryOptions, useMutation } from '@tanstack/react-query' -import { queryClient } from '../reactQuery' -import { contestResultsQuery } from '.' - -export type SolveContestStateResponse = { - currentSolve: { - canChangeToExtra: boolean - scramble: Scramble - solve: SolveNotInited | SolveSuccessful | SolveDnf - } - submittedSolves: Array<(SolveSuccessful | SolveDnf) & { scramble: Scramble }> -} - -type SolveNotInited = null -type SolveSuccessful = { id: number; timeMs: number; dnf: false } -type SolveDnf = { id: number; timeMs: null; dnf: true } - -const API_ROUTE = '/contests/solve-contest/' -export const SOLVE_CONTEST_STATE_QUERY_KEY = 'solveContestState' - -export const solveContestStateQuery = (contestNumber: number, discipline: Discipline) => - queryOptions({ - queryKey: [SOLVE_CONTEST_STATE_QUERY_KEY, { contestNumber, discipline }], - queryFn: async () => { - const res = await axiosClient.get(`${API_ROUTE}${contestNumber}/${discipline}/`) - return res.data - }, - }) - -export const usePostSolveResult = (contestNumber: number, discipline: Discipline) => - useMutation({ - mutationFn: async ({ - scrambleId, - result, - }: { - scrambleId: number - result: { reconstruction: string; timeMs: number; dnf: false } | { dnf: true; timeMs: null } - }) => { - const res = await axiosClient.post<{ solveId: number }>( - `${API_ROUTE}${contestNumber}/${discipline}/?scramble_id=${scrambleId}`, - result, - ) - - const query = solveContestStateQuery(contestNumber, discipline) - const previousState = await queryClient.fetchQuery(query) - queryClient.setQueryData(query.queryKey, { - ...previousState, - currentSolve: { ...previousState.currentSolve, solve: { id: res.data.solveId, ...result } }, - }) - }, - }) - -export const useSubmitSolve = (contestNumber: number, discipline: Discipline) => - useMutation({ - mutationFn: async () => { - const { data } = await axiosClient.put( - `${API_ROUTE}${contestNumber}/${discipline}/?action=submit`, - ) - - const roundFinished = 'detail' in data - if (roundFinished) { - queryClient.invalidateQueries(contestResultsQuery(contestNumber, discipline)) - return - } - const newSolvesState = data - - const query = solveContestStateQuery(contestNumber, discipline) - queryClient.setQueryData(query.queryKey, newSolvesState) - }, - }) - -export const useChangeToExtra = (contestNumber: number, discipline: Discipline) => - useMutation({ - mutationFn: async () => { - const { data: newSolvesState } = await axiosClient.put( - `${API_ROUTE}${contestNumber}/${discipline}/?action=change_to_extra`, - ) - - const query = solveContestStateQuery(contestNumber, discipline) - queryClient.setQueryData(query.queryKey, newSolvesState) - }, - }) diff --git a/src/api/contests/solveReconstruction.ts b/src/api/contests/solveReconstruction.ts deleted file mode 100644 index 12cf8649..00000000 --- a/src/api/contests/solveReconstruction.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { axiosClient } from '../axios' -import { Discipline, Scramble } from '@/types' -import { queryOptions } from '@tanstack/react-query' - -const API_ROUTE = 'contests/solve-reconstruction/' -export type SolveReconstructionResponse = { - id: number - reconstruction: string - scramble: Pick - contestNumber: number - discipline: { name: Discipline } - user: { username: string } -} - -async function fetchSolveReconstruction(solveId: number) { - const res = await axiosClient.get(`${API_ROUTE}${solveId}/`) - return res.data -} - -export const solveReconstructionQuery = (solveId: number | null) => - queryOptions({ - queryKey: ['solveReconstruction', solveId], - queryFn: () => fetchSolveReconstruction(solveId as number), - enabled: typeof solveId === 'number', - }) diff --git a/src/components/DevResetSession.tsx b/src/components/DevResetSession.tsx index 5d058865..5ee109ff 100644 --- a/src/components/DevResetSession.tsx +++ b/src/components/DevResetSession.tsx @@ -1,5 +1,6 @@ -import { axiosClient } from '@/api/axios' -import { CONTEST_RESULTS_QUERY_KEY, SOLVE_CONTEST_STATE_QUERY_KEY } from '@/api/contests' +import { CONTEST_RESULTS_QUERY_KEY } from '@/features/contests/api' +import { SOLVE_CONTEST_STATE_QUERY_KEY } from '@/features/solveContest/api' +import { axiosClient } from '@/lib/axios' import { useQueryClient } from '@tanstack/react-query' import { useNavigate } from '@tanstack/react-router' diff --git a/src/components/PickUsernameModal.tsx b/src/components/PickUsernameModal.tsx index 62c16382..b4ad0a84 100644 --- a/src/components/PickUsernameModal.tsx +++ b/src/components/PickUsernameModal.tsx @@ -1,5 +1,5 @@ -import { USER_QUERY_KEY, putChangeUsername, userQuery } from '@/api/accounts' -import { queryClient } from '@/api/reactQuery' +import { userQuery, putChangeUsername, USER_QUERY_KEY } from '@/features/auth' +import { queryClient } from '@/lib/reactQuery' import { useQuery } from '@tanstack/react-query' import { useEffect, useState } from 'react' diff --git a/src/components/layout/components/LoginSection.tsx b/src/components/layout/components/LoginSection.tsx index b13a554a..7994c561 100644 --- a/src/components/layout/components/LoginSection.tsx +++ b/src/components/layout/components/LoginSection.tsx @@ -1,8 +1,7 @@ import { useGoogleLogin } from '@react-oauth/google' import googleLogo from '@/assets/google-logo.svg' -import { userQuery } from '@/api/accounts' -import { login, logout } from '@/api/auth' import { useQuery } from '@tanstack/react-query' +import { login, logout, userQuery } from '@/features/auth' export function LoginSection() { const { data: userData } = useQuery(userQuery) diff --git a/src/components/layout/components/Navbar.tsx b/src/components/layout/components/Navbar.tsx index a034ed46..685c35d3 100644 --- a/src/components/layout/components/Navbar.tsx +++ b/src/components/layout/components/Navbar.tsx @@ -1,4 +1,4 @@ -import { ongoingContestNumberQuery } from '@/api/contests' +import { ongoingContestNumberQuery } from '@/features/contests' import { cn } from '@/utils' import { useQuery } from '@tanstack/react-query' import { Link, useParams } from '@tanstack/react-router' diff --git a/src/api/accounts/user.ts b/src/features/auth/api/getUser.ts similarity index 60% rename from src/api/accounts/user.ts rename to src/features/auth/api/getUser.ts index 8a06256a..a55bf91a 100644 --- a/src/api/accounts/user.ts +++ b/src/features/auth/api/getUser.ts @@ -1,12 +1,11 @@ +import { axiosClient } from '@/lib/axios' import { queryOptions } from '@tanstack/react-query' -import { axiosClient } from '../axios' -const API_ROUTE = 'accounts/current_user/' type UserData = { username: string; authCompleted: boolean } -async function fetchUser() { +async function getUser() { try { - const res = await axiosClient.get(API_ROUTE) + const res = await axiosClient.get('accounts/current_user/') return res.data } catch (err) { return null @@ -16,5 +15,5 @@ async function fetchUser() { export const USER_QUERY_KEY = 'user' export const userQuery = queryOptions({ queryKey: [USER_QUERY_KEY], - queryFn: fetchUser, + queryFn: getUser, }) diff --git a/src/api/accounts/index.ts b/src/features/auth/api/index.ts similarity index 79% rename from src/api/accounts/index.ts rename to src/features/auth/api/index.ts index 657e6ed3..57e3b96a 100644 --- a/src/api/accounts/index.ts +++ b/src/features/auth/api/index.ts @@ -1,4 +1,4 @@ -export * from './user' +export * from './getUser' export * from './postLogin' export * from './refreshAccessToken' export * from './putChangeUsername' diff --git a/src/api/accounts/postLogin.ts b/src/features/auth/api/postLogin.ts similarity index 52% rename from src/api/accounts/postLogin.ts rename to src/features/auth/api/postLogin.ts index e0b16599..baccc2dd 100644 --- a/src/api/accounts/postLogin.ts +++ b/src/features/auth/api/postLogin.ts @@ -1,11 +1,10 @@ -import { axiosClient } from '../axios' +import { axiosClient } from '@/lib/axios' type RequestBody = { code: string } type Response = { access: string; refresh: string } -const API_ROUTE = 'accounts/google/login/' export function postLogin(googleCode: string) { - return axiosClient.post(API_ROUTE, { + return axiosClient.post('accounts/google/login/', { code: googleCode, }) } diff --git a/src/features/auth/api/putChangeUsername.ts b/src/features/auth/api/putChangeUsername.ts new file mode 100644 index 00000000..036c4a85 --- /dev/null +++ b/src/features/auth/api/putChangeUsername.ts @@ -0,0 +1,7 @@ +import { axiosClient } from '@/lib/axios' + +export function putChangeUsername(username: string) { + return axiosClient.put('accounts/change_username/', { + username, + }) +} diff --git a/src/api/accounts/refreshAccessToken.ts b/src/features/auth/api/refreshAccessToken.ts similarity index 91% rename from src/api/accounts/refreshAccessToken.ts rename to src/features/auth/api/refreshAccessToken.ts index c3c65f05..473de879 100644 --- a/src/api/accounts/refreshAccessToken.ts +++ b/src/features/auth/api/refreshAccessToken.ts @@ -1,7 +1,6 @@ import axios, { AxiosRequestConfig } from 'axios' import { getAuthTokens, setAuthTokens, deleteAuthTokens } from '../authTokens' -const API_ROUTE = '/accounts/token/refresh/' export async function refreshAccessToken(axiosParams: AxiosRequestConfig) { const tokens = getAuthTokens() if (!tokens) { @@ -11,7 +10,7 @@ export async function refreshAccessToken(axiosParams: AxiosRequestConfig) { try { const response = await axios.post<{ refresh: string }, { data: { access: string } }>( - API_ROUTE, + '/accounts/token/refresh/', { refresh, }, diff --git a/src/api/authTokens.ts b/src/features/auth/authTokens.ts similarity index 100% rename from src/api/authTokens.ts rename to src/features/auth/authTokens.ts diff --git a/src/api/auth.ts b/src/features/auth/index.ts similarity index 69% rename from src/api/auth.ts rename to src/features/auth/index.ts index 9dc7c88f..5aec26f6 100644 --- a/src/api/auth.ts +++ b/src/features/auth/index.ts @@ -1,6 +1,10 @@ -import { USER_QUERY_KEY, postLogin } from '@/api/accounts' +import { queryClient } from '@/lib/reactQuery' +import { postLogin, USER_QUERY_KEY } from './api' import { setAuthTokens, deleteAuthTokens } from './authTokens' -import { queryClient } from './reactQuery' + +export { createAuthorizedRequestInterceptor } from './authTokens' + +export * from './api' export async function login(googleCode: string) { const response = await postLogin(googleCode) diff --git a/src/api/contests/contestResults.ts b/src/features/contests/api/getContestResults.ts similarity index 61% rename from src/api/contests/contestResults.ts rename to src/features/contests/api/getContestResults.ts index b956c1d9..95b50405 100644 --- a/src/api/contests/contestResults.ts +++ b/src/features/contests/api/getContestResults.ts @@ -1,9 +1,9 @@ +import { USER_QUERY_KEY } from '@/features/auth' +import { axiosClient } from '@/lib/axios' import { Discipline, Scramble } from '@/types' -import { axiosClient } from '../axios' import { queryOptions } from '@tanstack/react-query' -import { USER_QUERY_KEY } from '../accounts' -export type ContestResultsResponse = Array<{ +export type ContestResultsDTO = Array<{ id: number avgMs: number | null discipline: { name: Discipline } @@ -16,11 +16,10 @@ export type ContestResultsResponse = Array<{ state: 'submitted' | 'changed_to_extra' }> }> -const API_ROUTE = 'contests/contest/' export const CONTEST_RESULTS_QUERY_KEY = 'contestResults' -async function fetchContestResults(contestNumber: number, discipline: Discipline) { - const res = await axiosClient.get(`${API_ROUTE}${contestNumber}/${discipline}/`) +async function getContestResults(contestNumber: number, discipline: Discipline) { + const res = await axiosClient.get(`contests/contest/${contestNumber}/${discipline}/`) return res.data } @@ -28,6 +27,6 @@ export const contestResultsQuery = (contestNumber: number, discipline: Disciplin queryOptions({ queryKey: [USER_QUERY_KEY, CONTEST_RESULTS_QUERY_KEY, { contestNumber, discipline }], queryFn: () => { - return fetchContestResults(contestNumber, discipline) + return getContestResults(contestNumber, discipline) }, }) diff --git a/src/features/contests/api/getOngoingContestNumber.ts b/src/features/contests/api/getOngoingContestNumber.ts new file mode 100644 index 00000000..7de0ed31 --- /dev/null +++ b/src/features/contests/api/getOngoingContestNumber.ts @@ -0,0 +1,12 @@ +import { axiosClient } from '@/lib/axios' +import { queryOptions } from '@tanstack/react-query' + +async function getOngoingContestNumber() { + const res = await axiosClient.get('/contests/ongoing-contest-number/') + return res.data +} + +export const ongoingContestNumberQuery = queryOptions({ + queryKey: ['ongoing-contest-number'], + queryFn: () => getOngoingContestNumber(), +}) diff --git a/src/features/contests/api/index.ts b/src/features/contests/api/index.ts new file mode 100644 index 00000000..4f91233a --- /dev/null +++ b/src/features/contests/api/index.ts @@ -0,0 +1,2 @@ +export * from './getContestResults' +export * from './getOngoingContestNumber' diff --git a/src/pages/contest/ContestDiscipline.tsx b/src/features/contests/components/ContestDiscipline.tsx similarity index 78% rename from src/pages/contest/ContestDiscipline.tsx rename to src/features/contests/components/ContestDiscipline.tsx index 8d2d498e..d5680b4c 100644 --- a/src/pages/contest/ContestDiscipline.tsx +++ b/src/features/contests/components/ContestDiscipline.tsx @@ -1,11 +1,11 @@ -import { ContestResultsResponse } from '@/api/contests' -import { PublishedSession } from './components/PublishedSession' -import { SolveContest } from './components/SolveContest' import { InfoBox } from '@/components' -import { userQuery } from '@/api/accounts' -import { contestDisciplineRoute } from './contestsRoute' import { useQuery } from '@tanstack/react-query' import { Discipline } from '@/types' +import { SolveContest } from '@/features/solveContest' +import { contestDisciplineRoute } from '../routes' +import { PublishedSession } from './PublishedSession' +import { ContestResultsDTO } from '../api' +import { userQuery } from '@/features/auth' export function ContestDiscipline() { const { data: userData } = useQuery(userQuery) @@ -34,7 +34,7 @@ export function ContestDiscipline() { return An unknown error occured } - let ownSession: ContestResultsResponse[number] | undefined = undefined + let ownSession: ContestResultsDTO[number] | undefined = undefined if (userData) { ownSession = sessions.find((session) => session.user.username === userData?.username) } diff --git a/src/pages/contest/components/PublishedSession.tsx b/src/features/contests/components/PublishedSession.tsx similarity index 97% rename from src/pages/contest/components/PublishedSession.tsx rename to src/features/contests/components/PublishedSession.tsx index db96e9e8..b71282d4 100644 --- a/src/pages/contest/components/PublishedSession.tsx +++ b/src/features/contests/components/PublishedSession.tsx @@ -2,7 +2,7 @@ import { ReconstructTimeButton, ResultCard } from '@/components' import { ContestResultsResponse } from '@/api/contests' import { cn, formatTimeResult } from '@/utils' import { useMemo } from 'react' -import { useReconstructor } from '@/integrations/reconstructor' +import { useReconstructor } from '@/features/reconstructor' type PublishedSessionProps = ContestResultsResponse[number] export function PublishedSession({ diff --git a/src/features/contests/components/index.ts b/src/features/contests/components/index.ts new file mode 100644 index 00000000..a61bace1 --- /dev/null +++ b/src/features/contests/components/index.ts @@ -0,0 +1 @@ +export * from './ContestDiscipline' diff --git a/src/features/contests/index.ts b/src/features/contests/index.ts new file mode 100644 index 00000000..aad1f00b --- /dev/null +++ b/src/features/contests/index.ts @@ -0,0 +1,2 @@ +export { contestsRoute } from './routes' +export { ongoingContestNumberQuery } from './api' diff --git a/src/pages/contest/contestsRoute.tsx b/src/features/contests/routes/index.tsx similarity index 90% rename from src/pages/contest/contestsRoute.tsx rename to src/features/contests/routes/index.tsx index 9cb6b41d..5aab5829 100644 --- a/src/pages/contest/contestsRoute.tsx +++ b/src/features/contests/routes/index.tsx @@ -2,8 +2,8 @@ import { DisciplinesTabsLayout } from '@/components' import { rootRoute } from '@/router' import { DEFAULT_DISCIPLINE, isDiscipline } from '@/types' import { Route } from '@tanstack/react-router' -import { ContestDiscipline } from './ContestDiscipline' -import { contestResultsQuery, ongoingContestNumberQuery } from '@/api/contests' +import { ContestDiscipline } from '../components' +import { ongoingContestNumberQuery, contestResultsQuery } from '../api' const allContestsRoute = new Route({ getParentRoute: () => rootRoute, path: '/contest' }) const allContestsIndexRoute = new Route({ @@ -52,7 +52,7 @@ export const contestDisciplineRoute = new Route({ ), }) -export default allContestsRoute.addChildren([ +export const contestsRoute = allContestsRoute.addChildren([ allContestsIndexRoute, contestRoute.addChildren([contestDisciplineRoute, contestIndexRoute]), ]) diff --git a/src/features/cube/components/AbortPrompt.tsx b/src/features/cube/components/AbortPrompt.tsx new file mode 100644 index 00000000..8b7a9513 --- /dev/null +++ b/src/features/cube/components/AbortPrompt.tsx @@ -0,0 +1,21 @@ +export function AbortPrompt({ onCancel, onConfirm }: { onConfirm: () => void; onCancel: () => void }) { + return ( +
    +
    +

    + If you quit now, +
    + your result will be DNFed +

    +
    + + +
    +
    +
    + ) +} diff --git a/src/integrations/cube/Cube.tsx b/src/features/cube/components/Cube.tsx similarity index 100% rename from src/integrations/cube/Cube.tsx rename to src/features/cube/components/Cube.tsx diff --git a/src/integrations/cube/CubeProvider.tsx b/src/features/cube/components/CubeProvider.tsx similarity index 63% rename from src/integrations/cube/CubeProvider.tsx rename to src/features/cube/components/CubeProvider.tsx index fe6dfa0c..6ca83f0f 100644 --- a/src/integrations/cube/CubeProvider.tsx +++ b/src/features/cube/components/CubeProvider.tsx @@ -1,7 +1,10 @@ import { createContext, useCallback, useMemo, useRef, useState } from 'react' -import { CubeSolveFinishCallback, CubeSolveResult, Cube } from './Cube' import { cn, isTouchDevice, useConditionalBeforeUnload } from '@/utils' import { useLocalStorage } from 'usehooks-ts' +import { CubeSolveResult } from '..' +import { CubeSolveFinishCallback, Cube } from './Cube' +import { AbortPrompt } from './AbortPrompt' +import { DeviceWarningModal } from './DeviceWarningModal' type CubeContextValue = { initSolve: (scramble: string, solveFinishCallback: CubeSolveFinishCallback) => void @@ -116,63 +119,3 @@ export function CubeProvider({ children }: CubeProviderProps) { ) } - -type AbortPromptProps = { onConfirm: () => void; onCancel: () => void } -const AbortPrompt = ({ onCancel, onConfirm }: AbortPromptProps) => ( -
    -
    -

    - If you quit now, -
    - your result will be DNFed -

    -
    - - -
    -
    -
    -) - -function DeviceWarningModal({ - onCancel, - onConfirm, -}: { - onCancel: () => void - onConfirm: (isIgnoreChecked: boolean) => void -}) { - const [isIgnoreChecked, setIsIgnoreChecked] = useState(false) - - return ( -
    { - if (event.target === event.currentTarget) onCancel() - }} - className='fixed flex h-full w-full flex-col items-center justify-center rounded-[5px] bg-black bg-opacity-40 px-5 text-white' - > -
    -

    Solving without a keyboard is currently not supported.

    - -
    - - -
    -
    -
    - ) -} diff --git a/src/features/cube/components/DeviceWarningModal.tsx b/src/features/cube/components/DeviceWarningModal.tsx new file mode 100644 index 00000000..51d29e45 --- /dev/null +++ b/src/features/cube/components/DeviceWarningModal.tsx @@ -0,0 +1,40 @@ +import { useState } from 'react' + +export function DeviceWarningModal({ + onCancel, + onConfirm, +}: { + onCancel: () => void + onConfirm: (isIgnoreChecked: boolean) => void +}) { + const [isIgnoreChecked, setIsIgnoreChecked] = useState(false) + + return ( +
    { + if (event.target === event.currentTarget) onCancel() + }} + className='fixed flex h-full w-full flex-col items-center justify-center rounded-[5px] bg-black bg-opacity-40 px-5 text-white' + > +
    +

    Solving without a keyboard is currently not supported.

    + +
    + + +
    +
    +
    + ) +} diff --git a/src/features/cube/index.ts b/src/features/cube/index.ts new file mode 100644 index 00000000..6d0e8b73 --- /dev/null +++ b/src/features/cube/index.ts @@ -0,0 +1,9 @@ +import { useContext } from 'react' +import { CubeContext } from './components/CubeProvider' + +export * from './components/CubeProvider' +export { type CubeSolveResult } from './components/Cube' + +export function useCube() { + return useContext(CubeContext) +} diff --git a/src/api/contests/dashboard.ts b/src/features/dashboard/api/getDashboard.ts similarity index 65% rename from src/api/contests/dashboard.ts rename to src/features/dashboard/api/getDashboard.ts index 67f9c1b0..14169a20 100644 --- a/src/api/contests/dashboard.ts +++ b/src/features/dashboard/api/getDashboard.ts @@ -1,8 +1,8 @@ -import { axiosClient } from '../axios' +import { axiosClient } from '@/lib/axios' import { Discipline } from '@/types' import { queryOptions } from '@tanstack/react-query' -export type DashboardResponse = { +export type DashboardDTO = { bestSolves: Array<{ id: number contestNumber: number @@ -19,11 +19,9 @@ export type DashboardResponse = { }> } -const API_ROUTE = 'contests/dashboard/' - -async function fetchDashboard() { - const res = await axiosClient.get(API_ROUTE) +async function getDashboard() { + const res = await axiosClient.get('contests/dashboard/') return res.data } -export const dashboardQuery = queryOptions({ queryKey: ['dashboard'], queryFn: fetchDashboard }) +export const dashboardQuery = queryOptions({ queryKey: ['dashboard'], queryFn: getDashboard }) diff --git a/src/features/dashboard/api/index.ts b/src/features/dashboard/api/index.ts new file mode 100644 index 00000000..5a921e53 --- /dev/null +++ b/src/features/dashboard/api/index.ts @@ -0,0 +1 @@ +export * from './getDashboard' diff --git a/src/pages/dashboard/components/BestSolves.tsx b/src/features/dashboard/components/BestSolves.tsx similarity index 87% rename from src/pages/dashboard/components/BestSolves.tsx rename to src/features/dashboard/components/BestSolves.tsx index 38ff2eef..8903f003 100644 --- a/src/pages/dashboard/components/BestSolves.tsx +++ b/src/features/dashboard/components/BestSolves.tsx @@ -1,10 +1,10 @@ import { ReconstructTimeButton } from '@/components' import CubeIcon from '@/assets/3by3.svg?react' -import { DashboardResponse } from '@/api/contests' -import { useReconstructor } from '@/integrations/reconstructor' import { Link } from '@tanstack/react-router' +import { DashboardDTO } from '../api/getDashboard' +import { useReconstructor } from '@/features/reconstructor' -type BestSolvesProps = { bestSolves?: DashboardResponse['bestSolves'] } +type BestSolvesProps = { bestSolves?: DashboardDTO['bestSolves'] } export function BestSolves({ bestSolves }: BestSolvesProps) { const { showReconstruction } = useReconstructor() diff --git a/src/pages/dashboard/components/ContestsList.tsx b/src/features/dashboard/components/ContestsList.tsx similarity index 89% rename from src/pages/dashboard/components/ContestsList.tsx rename to src/features/dashboard/components/ContestsList.tsx index 5c3ab7e6..32df49ed 100644 --- a/src/pages/dashboard/components/ContestsList.tsx +++ b/src/features/dashboard/components/ContestsList.tsx @@ -1,9 +1,9 @@ -import { DashboardResponse } from '@/api/contests' import { DEFAULT_DISCIPLINE } from '@/types' import { cn } from '@/utils' import { Link } from '@tanstack/react-router' +import { DashboardDTO } from '../api/dashboard' -type ContestsListProps = { contests?: DashboardResponse['contests'] } +type ContestsListProps = { contests?: DashboardDTO['contests'] } export function ContestsList({ contests }: ContestsListProps) { const sortedContests = contests && [...contests].reverse() diff --git a/src/features/dashboard/components/index.ts b/src/features/dashboard/components/index.ts new file mode 100644 index 00000000..55bf0180 --- /dev/null +++ b/src/features/dashboard/components/index.ts @@ -0,0 +1,2 @@ +export * from './BestSolves' +export * from './ContestsList' diff --git a/src/features/dashboard/index.ts b/src/features/dashboard/index.ts new file mode 100644 index 00000000..49800c7a --- /dev/null +++ b/src/features/dashboard/index.ts @@ -0,0 +1 @@ +export * from './routes' diff --git a/src/pages/dashboard/DashboardPage.tsx b/src/features/dashboard/routes/Dashboard.tsx similarity index 53% rename from src/pages/dashboard/DashboardPage.tsx rename to src/features/dashboard/routes/Dashboard.tsx index 158cef8a..bf3f866b 100644 --- a/src/pages/dashboard/DashboardPage.tsx +++ b/src/features/dashboard/routes/Dashboard.tsx @@ -1,14 +1,10 @@ -import { dashboardQuery } from '@/api/contests' -import { ContestsList } from './components/ContestsList' -import { BestSolves } from './components/BestSolves' -import { QueryClient, useQuery } from '@tanstack/react-query' +import { useQuery } from '@tanstack/react-query' +import { dashboardRoute } from '.' +import { ContestsList, BestSolves } from '../components' -export function dashboardLoader({ context: { queryClient } }: { context: { queryClient: QueryClient } }) { - queryClient.ensureQueryData(dashboardQuery) -} - -export function DashboardPage() { - const { data: dashboard } = useQuery(dashboardQuery) +export function Dashboard() { + const query = dashboardRoute.useLoaderData() + const { data: dashboard } = useQuery(query) return (
    diff --git a/src/features/dashboard/routes/index.ts b/src/features/dashboard/routes/index.ts new file mode 100644 index 00000000..892c5bfc --- /dev/null +++ b/src/features/dashboard/routes/index.ts @@ -0,0 +1,14 @@ +import { rootRoute } from '@/router' +import { Dashboard } from './Dashboard' +import { Route } from '@tanstack/react-router' +import { dashboardQuery } from '../api' + +export const dashboardRoute = new Route({ + getParentRoute: () => rootRoute, + path: '/', + component: Dashboard, + loader: ({ context: { queryClient } }) => { + queryClient.ensureQueryData(dashboardQuery) + return dashboardQuery + }, +}) diff --git a/src/api/contests/leaderboard.ts b/src/features/leaderboard/api/getLeaderboard.ts similarity index 60% rename from src/api/contests/leaderboard.ts rename to src/features/leaderboard/api/getLeaderboard.ts index 945ebc57..9b1a8db7 100644 --- a/src/api/contests/leaderboard.ts +++ b/src/features/leaderboard/api/getLeaderboard.ts @@ -1,8 +1,8 @@ -import { queryOptions } from '@tanstack/react-query' -import { axiosClient } from '../axios' +import { axiosClient } from '@/lib/axios' import { Discipline, Scramble } from '@/types' +import { queryOptions } from '@tanstack/react-query' -export type LeaderboardResponse = Array<{ +export type LeaderboardDTO = Array<{ id: number timeMs: number created: string @@ -11,16 +11,15 @@ export type LeaderboardResponse = Array<{ user: { id: number; username: string } contest: { contestNumber: number } }> -const API_ROUTE = 'contests/leaderboard/' -async function fetchLeaderboard(discipline: Discipline) { - const res = await axiosClient.get(`${API_ROUTE}${discipline}/`) +async function getLeaderboard(discipline: Discipline) { + const res = await axiosClient.get(`contests/leaderboard/${discipline}/`) return res.data } export const leaderboardQuery = (discipline: Discipline) => queryOptions({ queryKey: ['leaderboard', { discipline }], - queryFn: () => fetchLeaderboard(discipline), + queryFn: () => getLeaderboard(discipline), retry: false, }) diff --git a/src/features/leaderboard/api/index.ts b/src/features/leaderboard/api/index.ts new file mode 100644 index 00000000..1c8ea937 --- /dev/null +++ b/src/features/leaderboard/api/index.ts @@ -0,0 +1 @@ +export * from './getLeaderboard' diff --git a/src/features/leaderboard/components/LeaderboardDiscipline.tsx b/src/features/leaderboard/components/LeaderboardDiscipline.tsx new file mode 100644 index 00000000..a21ad3d9 --- /dev/null +++ b/src/features/leaderboard/components/LeaderboardDiscipline.tsx @@ -0,0 +1,22 @@ +import { useQuery, useSuspenseQuery } from '@tanstack/react-query' +import { disciplineRoute } from '../routes' +import { LeaderboardResult } from './LeaderboardResult' +import { userQuery } from '@/features/auth' + +export function LeaderboardDiscipline() { + const { data: userData } = useQuery(userQuery) + + const query = disciplineRoute.useLoaderData() + const { data: results } = useSuspenseQuery(query) + + const ownResult = userData && results.find((result) => result.user.username === userData.username) + + return ( + <> + {ownResult && } + {results.map((result, index) => ( + + ))} + + ) +} diff --git a/src/pages/leaderboard/LeaderboardDiscipline.tsx b/src/features/leaderboard/components/LeaderboardResult.tsx similarity index 60% rename from src/pages/leaderboard/LeaderboardDiscipline.tsx rename to src/features/leaderboard/components/LeaderboardResult.tsx index affdbf03..6061d932 100644 --- a/src/pages/leaderboard/LeaderboardDiscipline.tsx +++ b/src/features/leaderboard/components/LeaderboardResult.tsx @@ -1,30 +1,9 @@ -import { ReconstructTimeButton, ResultCard } from '@/components' -import { LeaderboardResponse } from '@/api/contests' -import { userQuery } from '@/api/accounts' -import { useReconstructor } from '@/integrations/reconstructor' +import { ResultCard, ReconstructTimeButton } from '@/components' import { Link } from '@tanstack/react-router' -import { useQuery, useSuspenseQuery } from '@tanstack/react-query' -import { leaderboardDisciplineRoute } from './leaderboardRoute' +import { LeaderboardDTO } from '../api' +import { useReconstructor } from '@/features/reconstructor' -export function LeaderboardDiscipline() { - const { data: userData } = useQuery(userQuery) - - const query = leaderboardDisciplineRoute.useLoaderData() - const { data: results } = useSuspenseQuery(query) - - const ownResult = userData && results.find((result) => result.user.username === userData.username) - - return ( - <> - {ownResult && } - {results.map((result, index) => ( - - ))} - - ) -} - -function LeaderboardResult({ +export function LeaderboardResult({ user: { username }, id, timeMs, @@ -33,7 +12,7 @@ function LeaderboardResult({ discipline, placeNumber, isOwnResult, -}: LeaderboardResponse[number] & { placeNumber: number; isOwnResult?: boolean }) { +}: LeaderboardDTO[number] & { placeNumber: number; isOwnResult?: boolean }) { const { showReconstruction } = useReconstructor() const dateString = formatSolveDate(created) diff --git a/src/features/leaderboard/components/index.ts b/src/features/leaderboard/components/index.ts new file mode 100644 index 00000000..7bfb9a1e --- /dev/null +++ b/src/features/leaderboard/components/index.ts @@ -0,0 +1 @@ +export * from './LeaderboardDiscipline' diff --git a/src/features/leaderboard/index.ts b/src/features/leaderboard/index.ts new file mode 100644 index 00000000..49800c7a --- /dev/null +++ b/src/features/leaderboard/index.ts @@ -0,0 +1 @@ +export * from './routes' diff --git a/src/pages/leaderboard/leaderboardRoute.tsx b/src/features/leaderboard/routes/index.tsx similarity index 67% rename from src/pages/leaderboard/leaderboardRoute.tsx rename to src/features/leaderboard/routes/index.tsx index 3923b707..e022190e 100644 --- a/src/pages/leaderboard/leaderboardRoute.tsx +++ b/src/features/leaderboard/routes/index.tsx @@ -1,23 +1,23 @@ -import { leaderboardQuery } from '@/api/contests' import { rootRoute } from '@/router' import { DEFAULT_DISCIPLINE, isDiscipline } from '@/types' import { Route } from '@tanstack/react-router' +import { LeaderboardDiscipline } from '../components' import { DisciplinesTabsLayout } from '@/components' -import { LeaderboardDiscipline } from './LeaderboardDiscipline' +import { leaderboardQuery } from '../api' -const leaderboardRoute = new Route({ +const route = new Route({ getParentRoute: () => rootRoute, path: '/leaderboard', }) -const leaderboardIndexRoute = new Route({ - getParentRoute: () => leaderboardRoute, +const indexRoute = new Route({ + getParentRoute: () => route, path: '/', beforeLoad: ({ navigate }) => { navigate({ to: '$discipline', params: { discipline: DEFAULT_DISCIPLINE }, replace: true }) }, }) -export const leaderboardDisciplineRoute = new Route({ - getParentRoute: () => leaderboardRoute, +export const disciplineRoute = new Route({ + getParentRoute: () => route, path: '$discipline', pendingComponent: () =>
    Loading...
    , loader: ({ params: { discipline }, navigate, context: { queryClient } }) => { @@ -36,4 +36,4 @@ export const leaderboardDisciplineRoute = new Route({ ), }) -export default leaderboardRoute.addChildren([leaderboardIndexRoute, leaderboardDisciplineRoute]) +export const leaderboardRoute = route.addChildren([indexRoute, disciplineRoute]) diff --git a/src/features/reconstructor/api/getReconstruction.ts b/src/features/reconstructor/api/getReconstruction.ts new file mode 100644 index 00000000..62f0662e --- /dev/null +++ b/src/features/reconstructor/api/getReconstruction.ts @@ -0,0 +1,24 @@ +import { axiosClient } from '@/lib/axios' +import { Discipline, Scramble } from '@/types' +import { queryOptions } from '@tanstack/react-query' + +export type ReconstructionDTO = { + id: number + reconstruction: string + scramble: Pick + contestNumber: number + discipline: { name: Discipline } + user: { username: string } +} + +async function getReconstruction(solveId: number) { + const res = await axiosClient.get(`contests/solve-reconstruction/${solveId}/`) + return res.data +} + +export const reconstructionQuery = (solveId: number | null) => + queryOptions({ + queryKey: ['solveReconstruction', solveId], + queryFn: () => getReconstruction(solveId as number), + enabled: typeof solveId === 'number', + }) diff --git a/src/features/reconstructor/api/index.ts b/src/features/reconstructor/api/index.ts new file mode 100644 index 00000000..df4cbeb3 --- /dev/null +++ b/src/features/reconstructor/api/index.ts @@ -0,0 +1 @@ +export * from './getReconstruction' diff --git a/src/integrations/reconstructor/Reconstructor.tsx b/src/features/reconstructor/components/Reconstructor.tsx similarity index 100% rename from src/integrations/reconstructor/Reconstructor.tsx rename to src/features/reconstructor/components/Reconstructor.tsx diff --git a/src/integrations/reconstructor/ReconstructorProvider.tsx b/src/features/reconstructor/components/ReconstructorProvider.tsx similarity index 86% rename from src/integrations/reconstructor/ReconstructorProvider.tsx rename to src/features/reconstructor/components/ReconstructorProvider.tsx index f5769090..0a9e85e1 100644 --- a/src/integrations/reconstructor/ReconstructorProvider.tsx +++ b/src/features/reconstructor/components/ReconstructorProvider.tsx @@ -1,8 +1,8 @@ import { createContext, useCallback, useMemo, useState } from 'react' import { Reconstruction, ReconstructionMetadata, Reconstructor } from './Reconstructor' -import { SolveReconstructionResponse, solveReconstructionQuery } from '@/api/contests' import { cn, formatTimeResult } from '@/utils' import { useQuery } from '@tanstack/react-query' +import { reconstructionQuery, ReconstructionDTO } from '../api' type ReconstructorContextValue = { showReconstruction: (solveId: number, onClose?: () => void) => void @@ -18,14 +18,13 @@ export const ReconstructorContext = createContext({ }, }) -type ReconstructorProviderProps = { children: React.ReactNode } -export function ReconstructorProvider({ children }: ReconstructorProviderProps) { +export function ReconstructorProvider({ children }: { children: React.ReactNode }) { const [solveId, setSolveId] = useState(null) const [savedCloseCallback, setSavedCloseCallback] = useState<() => void>() - const { data } = useQuery(solveReconstructionQuery(solveId)) + const { data } = useQuery(reconstructionQuery(solveId)) const content = useMemo(() => { if (!data) return null - return parseReconstructionResponse(data) + return parseReconstructionDTO(data) }, [data]) const close = useCallback(() => { @@ -68,14 +67,14 @@ export function ReconstructorProvider({ children }: ReconstructorProviderProps) ) } -function parseReconstructionResponse({ +function parseReconstructionDTO({ contestNumber: contestNumber, discipline, id, scramble, user: { username }, reconstruction: solution, -}: SolveReconstructionResponse) { +}: ReconstructionDTO) { const link = `${window.location.origin}/contest/${contestNumber}/${discipline.name}?solveId=${id}` const reconstruction = { scramble: scramble.scramble, solution } satisfies Reconstruction const metadata = { diff --git a/src/integrations/reconstructor/index.ts b/src/features/reconstructor/components/index.ts similarity index 53% rename from src/integrations/reconstructor/index.ts rename to src/features/reconstructor/components/index.ts index 6db22863..2e04a100 100644 --- a/src/integrations/reconstructor/index.ts +++ b/src/features/reconstructor/components/index.ts @@ -1,2 +1,2 @@ export * from './ReconstructorProvider' -export * from './useReconstructor' +export * from './Reconstructor' diff --git a/src/features/reconstructor/index.ts b/src/features/reconstructor/index.ts new file mode 100644 index 00000000..25182d36 --- /dev/null +++ b/src/features/reconstructor/index.ts @@ -0,0 +1,7 @@ +import { useContext } from 'react' +import { ReconstructorContext } from './components/ReconstructorProvider' + +export { ReconstructorProvider } from './components' +export function useReconstructor() { + return useContext(ReconstructorContext) +} diff --git a/src/pages/contest/components/SolveContest/SolveContest.tsx b/src/features/solveContest/SolveContest.tsx similarity index 95% rename from src/pages/contest/components/SolveContest/SolveContest.tsx rename to src/features/solveContest/SolveContest.tsx index c064e339..0f8a21bc 100644 --- a/src/pages/contest/components/SolveContest/SolveContest.tsx +++ b/src/features/solveContest/SolveContest.tsx @@ -1,10 +1,10 @@ import { Discipline } from '@/types' -import { solveContestStateQuery, usePostSolveResult, useSubmitSolve, useChangeToExtra } from '@/api/contests' import { CurrentSolve, SubmittedSolve } from './components' import { InfoBox } from '@/components' -import { userQuery } from '@/api/accounts' import { CubeSolveResult } from '@/integrations/cube' import { useQuery } from '@tanstack/react-query' +import { solveContestStateQuery, usePostSolveResult, useSubmitSolve, useChangeToExtra } from './api' +import { userQuery } from '../auth' type SolveContestProps = { contestNumber: number; discipline: Discipline } export function SolveContest({ contestNumber, discipline }: SolveContestProps) { diff --git a/src/features/solveContest/api/changeToExtra.ts b/src/features/solveContest/api/changeToExtra.ts new file mode 100644 index 00000000..72094949 --- /dev/null +++ b/src/features/solveContest/api/changeToExtra.ts @@ -0,0 +1,19 @@ +import { axiosClient } from '@/lib/axios' +import { queryClient } from '@/lib/reactQuery' +import { Discipline } from '@/types' +import { useMutation } from '@tanstack/react-query' +import { SolveContestStateDTO } from '../types' +import { API_ROUTE } from './constants' +import { solveContestStateQuery } from './getSolveContestState' + +export const useChangeToExtra = (contestNumber: number, discipline: Discipline) => + useMutation({ + mutationFn: async () => { + const { data: newSolvesState } = await axiosClient.put( + `${API_ROUTE}${contestNumber}/${discipline}/?action=change_to_extra`, + ) + + const query = solveContestStateQuery(contestNumber, discipline) + queryClient.setQueryData(query.queryKey, newSolvesState) + }, + }) diff --git a/src/features/solveContest/api/constants.ts b/src/features/solveContest/api/constants.ts new file mode 100644 index 00000000..b96c8c07 --- /dev/null +++ b/src/features/solveContest/api/constants.ts @@ -0,0 +1 @@ +export const API_ROUTE = '/contests/solve-contest/' diff --git a/src/features/solveContest/api/getSolveContestState.ts b/src/features/solveContest/api/getSolveContestState.ts new file mode 100644 index 00000000..a9e996e0 --- /dev/null +++ b/src/features/solveContest/api/getSolveContestState.ts @@ -0,0 +1,16 @@ +import { axiosClient } from '@/lib/axios' +import { Discipline } from '@/types' +import { queryOptions } from '@tanstack/react-query' +import { SolveContestStateDTO } from '../types' +import { API_ROUTE } from './constants' + +export const SOLVE_CONTEST_STATE_QUERY_KEY = 'solveContestState' + +export const solveContestStateQuery = (contestNumber: number, discipline: Discipline) => + queryOptions({ + queryKey: [SOLVE_CONTEST_STATE_QUERY_KEY, { contestNumber, discipline }], + queryFn: async () => { + const res = await axiosClient.get(`${API_ROUTE}${contestNumber}/${discipline}/`) + return res.data + }, + }) diff --git a/src/features/solveContest/api/index.ts b/src/features/solveContest/api/index.ts new file mode 100644 index 00000000..b21f64c8 --- /dev/null +++ b/src/features/solveContest/api/index.ts @@ -0,0 +1,4 @@ +export * from './getSolveContestState' +export * from './postSolveResult' +export * from './submitSolve' +export * from './changeToExtra' diff --git a/src/features/solveContest/api/postSolveResult.ts b/src/features/solveContest/api/postSolveResult.ts new file mode 100644 index 00000000..50d33386 --- /dev/null +++ b/src/features/solveContest/api/postSolveResult.ts @@ -0,0 +1,29 @@ +import { queryClient } from '@/lib/reactQuery' +import { axiosClient } from '@/lib/axios' +import { Discipline } from '@/types' +import { useMutation } from '@tanstack/react-query' +import { API_ROUTE } from './constants' +import { solveContestStateQuery } from './getSolveContestState' + +export const usePostSolveResult = (contestNumber: number, discipline: Discipline) => + useMutation({ + mutationFn: async ({ + scrambleId, + result, + }: { + scrambleId: number + result: { reconstruction: string; timeMs: number; dnf: false } | { dnf: true; timeMs: null } + }) => { + const res = await axiosClient.post<{ solveId: number }>( + `${API_ROUTE}${contestNumber}/${discipline}/?scramble_id=${scrambleId}`, + result, + ) + + const query = solveContestStateQuery(contestNumber, discipline) + const previousState = await queryClient.fetchQuery(query) + queryClient.setQueryData(query.queryKey, { + ...previousState, + currentSolve: { ...previousState.currentSolve, solve: { id: res.data.solveId, ...result } }, + }) + }, + }) diff --git a/src/features/solveContest/api/submitSolve.ts b/src/features/solveContest/api/submitSolve.ts new file mode 100644 index 00000000..6e620302 --- /dev/null +++ b/src/features/solveContest/api/submitSolve.ts @@ -0,0 +1,27 @@ +import { axiosClient } from '@/lib/axios' +import { queryClient } from '@/lib/reactQuery' +import { Discipline } from '@/types' +import { useMutation } from '@tanstack/react-query' +import { SolveContestStateDTO } from '../types' +import { API_ROUTE } from './constants' +import { solveContestStateQuery } from './getSolveContestState' +import { contestResultsQuery } from '@/features/contests/api' + +export const useSubmitSolve = (contestNumber: number, discipline: Discipline) => + useMutation({ + mutationFn: async () => { + const { data } = await axiosClient.put( + `${API_ROUTE}${contestNumber}/${discipline}/?action=submit`, + ) + + const isSessionOver = 'detail' in data + if (isSessionOver) { + queryClient.invalidateQueries(contestResultsQuery(contestNumber, discipline)) + return + } + const newSolvesState = data + + const query = solveContestStateQuery(contestNumber, discipline) + queryClient.setQueryData(query.queryKey, newSolvesState) + }, + }) diff --git a/src/pages/contest/components/SolveContest/components/CurrentSolve.tsx b/src/features/solveContest/components/CurrentSolve.tsx similarity index 88% rename from src/pages/contest/components/SolveContest/components/CurrentSolve.tsx rename to src/features/solveContest/components/CurrentSolve.tsx index 9784f5f2..4a7f7ed4 100644 --- a/src/pages/contest/components/SolveContest/components/CurrentSolve.tsx +++ b/src/features/solveContest/components/CurrentSolve.tsx @@ -1,11 +1,10 @@ import { ReconstructTimeButton } from '@/components' -import { SolveContestStateResponse } from '@/api/contests' import { cn } from '@/utils' -import { useCube } from '@/integrations/cube' -import { CubeSolveResult } from '@/integrations/cube' -import { useReconstructor } from '@/integrations/reconstructor' +import { useReconstructor } from '@/features/reconstructor' +import { SolveContestStateDTO } from '../types' +import { CubeSolveResult, useCube } from '@/features/cube' -type CurrentSolveProps = SolveContestStateResponse['currentSolve'] & { +type CurrentSolveProps = SolveContestStateDTO['currentSolve'] & { className?: string onSolveFinish: (result: CubeSolveResult) => void onSubmit: () => void diff --git a/src/pages/contest/components/SolveContest/components/SubmittedSolve.tsx b/src/features/solveContest/components/SubmittedSolve.tsx similarity index 94% rename from src/pages/contest/components/SolveContest/components/SubmittedSolve.tsx rename to src/features/solveContest/components/SubmittedSolve.tsx index c07820f2..97256b5d 100644 --- a/src/pages/contest/components/SolveContest/components/SubmittedSolve.tsx +++ b/src/features/solveContest/components/SubmittedSolve.tsx @@ -1,6 +1,6 @@ import { SolveContestStateResponse } from '@/api/contests' import { ReconstructTimeButton } from '@/components' -import { useReconstructor } from '@/integrations/reconstructor' +import { useReconstructor } from '@/features/reconstructor' import { cn } from '@/utils' type SubmittedSolveProps = SolveContestStateResponse['submittedSolves'][number] & { diff --git a/src/pages/contest/components/SolveContest/components/index.ts b/src/features/solveContest/components/index.ts similarity index 100% rename from src/pages/contest/components/SolveContest/components/index.ts rename to src/features/solveContest/components/index.ts diff --git a/src/features/solveContest/index.ts b/src/features/solveContest/index.ts new file mode 100644 index 00000000..fe5302a9 --- /dev/null +++ b/src/features/solveContest/index.ts @@ -0,0 +1 @@ +export { SolveContest } from './SolveContest' diff --git a/src/features/solveContest/types/index.ts b/src/features/solveContest/types/index.ts new file mode 100644 index 00000000..d94a8949 --- /dev/null +++ b/src/features/solveContest/types/index.ts @@ -0,0 +1,14 @@ +import { Scramble } from '@/types' + +export type SolveContestStateDTO = { + currentSolve: { + canChangeToExtra: boolean + scramble: Scramble + solve: SolveNotInited | SolveSuccessful | SolveDnf + } + submittedSolves: Array<(SolveSuccessful | SolveDnf) & { scramble: Scramble }> +} + +type SolveNotInited = null +type SolveSuccessful = { id: number; timeMs: number; dnf: false } +type SolveDnf = { id: number; timeMs: null; dnf: true } diff --git a/src/integrations/cube/index.ts b/src/integrations/cube/index.ts deleted file mode 100644 index 8fae5cf5..00000000 --- a/src/integrations/cube/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './useCube' -export * from './CubeProvider' -export { type CubeSolveResult } from './Cube' diff --git a/src/integrations/cube/useCube.tsx b/src/integrations/cube/useCube.tsx deleted file mode 100644 index 1722e151..00000000 --- a/src/integrations/cube/useCube.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { useContext } from 'react' -import { CubeContext } from './CubeProvider' - -export function useCube() { - return useContext(CubeContext) -} diff --git a/src/integrations/reconstructor/useReconstructor.tsx b/src/integrations/reconstructor/useReconstructor.tsx deleted file mode 100644 index 0443c441..00000000 --- a/src/integrations/reconstructor/useReconstructor.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { useContext } from 'react' -import { ReconstructorContext } from './ReconstructorProvider' - -export function useReconstructor() { - return useContext(ReconstructorContext) -} diff --git a/src/api/axios.ts b/src/lib/axios.ts similarity index 82% rename from src/api/axios.ts rename to src/lib/axios.ts index 88b33500..daf25424 100644 --- a/src/api/axios.ts +++ b/src/lib/axios.ts @@ -1,7 +1,6 @@ +import { createAuthorizedRequestInterceptor, refreshAccessToken } from '@/features/auth' import axios from 'axios' import createAuthRefreshInterceptor from 'axios-auth-refresh' -import { refreshAccessToken } from './accounts' -import { createAuthorizedRequestInterceptor } from './authTokens' import applyCaseMiddleware from 'axios-case-converter' const axiosParams = { diff --git a/src/api/reactQuery.ts b/src/lib/reactQuery.ts similarity index 100% rename from src/api/reactQuery.ts rename to src/lib/reactQuery.ts diff --git a/src/pages/contest/components/SolveContest/index.ts b/src/pages/contest/components/SolveContest/index.ts deleted file mode 100644 index eecc66a5..00000000 --- a/src/pages/contest/components/SolveContest/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './SolveContest' diff --git a/src/pages/contest/index.ts b/src/pages/contest/index.ts deleted file mode 100644 index 75237131..00000000 --- a/src/pages/contest/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './contestsRoute' diff --git a/src/pages/dashboard/index.ts b/src/pages/dashboard/index.ts deleted file mode 100644 index f1afe10c..00000000 --- a/src/pages/dashboard/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './DashboardPage' diff --git a/src/pages/index.ts b/src/pages/index.ts deleted file mode 100644 index a6588a8c..00000000 --- a/src/pages/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './dashboard' -export * from './leaderboard' -export * from './contest' diff --git a/src/pages/leaderboard/index.ts b/src/pages/leaderboard/index.ts deleted file mode 100644 index 45d67e96..00000000 --- a/src/pages/leaderboard/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './leaderboardRoute' diff --git a/src/router.tsx b/src/router.tsx index 8fa884a5..e9914dc6 100644 --- a/src/router.tsx +++ b/src/router.tsx @@ -1,12 +1,12 @@ import { Layout, DevResetSession } from './components' -import { DashboardPage, dashboardLoader } from './pages' import { TanStackRouterDevtools } from '@tanstack/router-devtools' import { Route, Router, rootRouteWithContext } from '@tanstack/react-router' -import leaderboardRoute from './pages/leaderboard/leaderboardRoute' -import contestsRoute from './pages/contest/contestsRoute' +import { leaderboardRoute } from './features/leaderboard' +import { contestsRoute } from './features/contests' import { QueryClient } from '@tanstack/react-query' -import { queryClient } from './api/reactQuery' import { ReactQueryDevtools } from '@tanstack/react-query-devtools' +import { dashboardRoute } from './features/dashboard' +import { queryClient } from './lib/reactQuery' export const rootRoute = rootRouteWithContext<{ queryClient: QueryClient }>()({ component: () => ( @@ -20,13 +20,8 @@ export const rootRoute = rootRouteWithContext<{ queryClient: QueryClient }>()({ ), }) -const indexRoute = new Route({ - getParentRoute: () => rootRoute, - path: '/', - component: DashboardPage, - loader: dashboardLoader, -}) +const indexRoute = dashboardRoute const devResetSessionRoute = new Route({ getParentRoute: () => rootRoute, path: 'dev/reset-session', From e219a5622c47cece2379874dbda4c7790417f2af Mon Sep 17 00:00:00 2001 From: bohdancho Date: Sun, 24 Dec 2023 13:57:42 +0100 Subject: [PATCH 16/27] refactor: add queryKeys files to modules --- src/components/DevResetSession.tsx | 3 +-- src/features/auth/api/getUser.ts | 2 +- src/features/auth/index.ts | 5 +++-- src/features/auth/queryKeys.ts | 3 +++ src/features/contests/api/getContestResults.ts | 4 ++-- src/features/contests/api/getOngoingContestNumber.ts | 3 ++- src/features/contests/index.ts | 1 + src/features/contests/queryKeys.ts | 4 ++++ src/features/dashboard/api/getDashboard.ts | 3 ++- src/features/dashboard/index.ts | 1 + src/features/dashboard/queryKeys.ts | 2 ++ src/features/leaderboard/api/getLeaderboard.ts | 3 ++- src/features/leaderboard/index.ts | 1 + src/features/leaderboard/queryKeys.ts | 2 ++ src/features/reconstructor/api/getReconstruction.ts | 3 ++- src/features/reconstructor/index.ts | 1 + src/features/reconstructor/queryKeys.ts | 2 ++ src/features/solveContest/api/getSolveContestState.ts | 3 +-- src/features/solveContest/queryKeys.ts | 2 ++ 19 files changed, 35 insertions(+), 13 deletions(-) create mode 100644 src/features/auth/queryKeys.ts create mode 100644 src/features/contests/queryKeys.ts create mode 100644 src/features/dashboard/queryKeys.ts create mode 100644 src/features/leaderboard/queryKeys.ts create mode 100644 src/features/reconstructor/queryKeys.ts create mode 100644 src/features/solveContest/queryKeys.ts diff --git a/src/components/DevResetSession.tsx b/src/components/DevResetSession.tsx index 5ee109ff..3bcee894 100644 --- a/src/components/DevResetSession.tsx +++ b/src/components/DevResetSession.tsx @@ -1,5 +1,4 @@ -import { CONTEST_RESULTS_QUERY_KEY } from '@/features/contests/api' -import { SOLVE_CONTEST_STATE_QUERY_KEY } from '@/features/solveContest/api' +import { CONTEST_RESULTS_QUERY_KEY } from '@/features/contests' import { axiosClient } from '@/lib/axios' import { useQueryClient } from '@tanstack/react-query' import { useNavigate } from '@tanstack/react-router' diff --git a/src/features/auth/api/getUser.ts b/src/features/auth/api/getUser.ts index a55bf91a..8539e28e 100644 --- a/src/features/auth/api/getUser.ts +++ b/src/features/auth/api/getUser.ts @@ -1,5 +1,6 @@ import { axiosClient } from '@/lib/axios' import { queryOptions } from '@tanstack/react-query' +import { USER_QUERY_KEY } from '../queryKeys' type UserData = { username: string; authCompleted: boolean } @@ -12,7 +13,6 @@ async function getUser() { } } -export const USER_QUERY_KEY = 'user' export const userQuery = queryOptions({ queryKey: [USER_QUERY_KEY], queryFn: getUser, diff --git a/src/features/auth/index.ts b/src/features/auth/index.ts index 5aec26f6..2e12c845 100644 --- a/src/features/auth/index.ts +++ b/src/features/auth/index.ts @@ -1,10 +1,11 @@ import { queryClient } from '@/lib/reactQuery' -import { postLogin, USER_QUERY_KEY } from './api' +import { postLogin } from './api' import { setAuthTokens, deleteAuthTokens } from './authTokens' +import { USER_QUERY_KEY } from './queryKeys' export { createAuthorizedRequestInterceptor } from './authTokens' - export * from './api' +export * from './queryKeys' export async function login(googleCode: string) { const response = await postLogin(googleCode) diff --git a/src/features/auth/queryKeys.ts b/src/features/auth/queryKeys.ts new file mode 100644 index 00000000..26b10b24 --- /dev/null +++ b/src/features/auth/queryKeys.ts @@ -0,0 +1,3 @@ +const PREFIX = 'auth-' + +export const USER_QUERY_KEY = PREFIX + 'user' diff --git a/src/features/contests/api/getContestResults.ts b/src/features/contests/api/getContestResults.ts index 95b50405..6a815f67 100644 --- a/src/features/contests/api/getContestResults.ts +++ b/src/features/contests/api/getContestResults.ts @@ -1,7 +1,8 @@ -import { USER_QUERY_KEY } from '@/features/auth' import { axiosClient } from '@/lib/axios' import { Discipline, Scramble } from '@/types' import { queryOptions } from '@tanstack/react-query' +import { CONTEST_RESULTS_QUERY_KEY } from '../queryKeys' +import { USER_QUERY_KEY } from '@/features/auth' export type ContestResultsDTO = Array<{ id: number @@ -16,7 +17,6 @@ export type ContestResultsDTO = Array<{ state: 'submitted' | 'changed_to_extra' }> }> -export const CONTEST_RESULTS_QUERY_KEY = 'contestResults' async function getContestResults(contestNumber: number, discipline: Discipline) { const res = await axiosClient.get(`contests/contest/${contestNumber}/${discipline}/`) diff --git a/src/features/contests/api/getOngoingContestNumber.ts b/src/features/contests/api/getOngoingContestNumber.ts index 7de0ed31..57f8b56d 100644 --- a/src/features/contests/api/getOngoingContestNumber.ts +++ b/src/features/contests/api/getOngoingContestNumber.ts @@ -1,5 +1,6 @@ import { axiosClient } from '@/lib/axios' import { queryOptions } from '@tanstack/react-query' +import { ONGOING_CONTEST_NUMBER_QUERY_KEY } from '../queryKeys' async function getOngoingContestNumber() { const res = await axiosClient.get('/contests/ongoing-contest-number/') @@ -7,6 +8,6 @@ async function getOngoingContestNumber() { } export const ongoingContestNumberQuery = queryOptions({ - queryKey: ['ongoing-contest-number'], + queryKey: [ONGOING_CONTEST_NUMBER_QUERY_KEY], queryFn: () => getOngoingContestNumber(), }) diff --git a/src/features/contests/index.ts b/src/features/contests/index.ts index aad1f00b..8022c6bf 100644 --- a/src/features/contests/index.ts +++ b/src/features/contests/index.ts @@ -1,2 +1,3 @@ export { contestsRoute } from './routes' export { ongoingContestNumberQuery } from './api' +export * from './queryKeys' diff --git a/src/features/contests/queryKeys.ts b/src/features/contests/queryKeys.ts new file mode 100644 index 00000000..9b39fda0 --- /dev/null +++ b/src/features/contests/queryKeys.ts @@ -0,0 +1,4 @@ +const PREFIX = 'contests-' + +export const ONGOING_CONTEST_NUMBER_QUERY_KEY = PREFIX + 'ongoing-contest-number' +export const CONTEST_RESULTS_QUERY_KEY = 'contest-results' diff --git a/src/features/dashboard/api/getDashboard.ts b/src/features/dashboard/api/getDashboard.ts index 14169a20..f34ae13c 100644 --- a/src/features/dashboard/api/getDashboard.ts +++ b/src/features/dashboard/api/getDashboard.ts @@ -1,6 +1,7 @@ import { axiosClient } from '@/lib/axios' import { Discipline } from '@/types' import { queryOptions } from '@tanstack/react-query' +import { DASHBOARD_QUERY_KEY } from '../queryKeys' export type DashboardDTO = { bestSolves: Array<{ @@ -24,4 +25,4 @@ async function getDashboard() { return res.data } -export const dashboardQuery = queryOptions({ queryKey: ['dashboard'], queryFn: getDashboard }) +export const dashboardQuery = queryOptions({ queryKey: [DASHBOARD_QUERY_KEY], queryFn: getDashboard }) diff --git a/src/features/dashboard/index.ts b/src/features/dashboard/index.ts index 49800c7a..950119a5 100644 --- a/src/features/dashboard/index.ts +++ b/src/features/dashboard/index.ts @@ -1 +1,2 @@ export * from './routes' +export * from './queryKeys' diff --git a/src/features/dashboard/queryKeys.ts b/src/features/dashboard/queryKeys.ts new file mode 100644 index 00000000..c1b5e80e --- /dev/null +++ b/src/features/dashboard/queryKeys.ts @@ -0,0 +1,2 @@ +const PREFIX = 'dashboard' +export const DASHBOARD_QUERY_KEY = PREFIX diff --git a/src/features/leaderboard/api/getLeaderboard.ts b/src/features/leaderboard/api/getLeaderboard.ts index 9b1a8db7..6de02b94 100644 --- a/src/features/leaderboard/api/getLeaderboard.ts +++ b/src/features/leaderboard/api/getLeaderboard.ts @@ -1,6 +1,7 @@ import { axiosClient } from '@/lib/axios' import { Discipline, Scramble } from '@/types' import { queryOptions } from '@tanstack/react-query' +import { LEADERBOARD_QUERY_KEY } from '../queryKeys' export type LeaderboardDTO = Array<{ id: number @@ -19,7 +20,7 @@ async function getLeaderboard(discipline: Discipline) { export const leaderboardQuery = (discipline: Discipline) => queryOptions({ - queryKey: ['leaderboard', { discipline }], + queryKey: [LEADERBOARD_QUERY_KEY, { discipline }], queryFn: () => getLeaderboard(discipline), retry: false, }) diff --git a/src/features/leaderboard/index.ts b/src/features/leaderboard/index.ts index 49800c7a..950119a5 100644 --- a/src/features/leaderboard/index.ts +++ b/src/features/leaderboard/index.ts @@ -1 +1,2 @@ export * from './routes' +export * from './queryKeys' diff --git a/src/features/leaderboard/queryKeys.ts b/src/features/leaderboard/queryKeys.ts new file mode 100644 index 00000000..72abd0fa --- /dev/null +++ b/src/features/leaderboard/queryKeys.ts @@ -0,0 +1,2 @@ +const PREFIX = 'leaderboard' +export const LEADERBOARD_QUERY_KEY = PREFIX diff --git a/src/features/reconstructor/api/getReconstruction.ts b/src/features/reconstructor/api/getReconstruction.ts index 62f0662e..1cd967b8 100644 --- a/src/features/reconstructor/api/getReconstruction.ts +++ b/src/features/reconstructor/api/getReconstruction.ts @@ -1,6 +1,7 @@ import { axiosClient } from '@/lib/axios' import { Discipline, Scramble } from '@/types' import { queryOptions } from '@tanstack/react-query' +import { RECONSTRUTOR_SOLVE_QUERY_KEY } from '../queryKeys' export type ReconstructionDTO = { id: number @@ -18,7 +19,7 @@ async function getReconstruction(solveId: number) { export const reconstructionQuery = (solveId: number | null) => queryOptions({ - queryKey: ['solveReconstruction', solveId], + queryKey: [RECONSTRUTOR_SOLVE_QUERY_KEY, solveId], queryFn: () => getReconstruction(solveId as number), enabled: typeof solveId === 'number', }) diff --git a/src/features/reconstructor/index.ts b/src/features/reconstructor/index.ts index 25182d36..ff16b14e 100644 --- a/src/features/reconstructor/index.ts +++ b/src/features/reconstructor/index.ts @@ -1,6 +1,7 @@ import { useContext } from 'react' import { ReconstructorContext } from './components/ReconstructorProvider' +export * from './queryKeys' export { ReconstructorProvider } from './components' export function useReconstructor() { return useContext(ReconstructorContext) diff --git a/src/features/reconstructor/queryKeys.ts b/src/features/reconstructor/queryKeys.ts new file mode 100644 index 00000000..b25104e2 --- /dev/null +++ b/src/features/reconstructor/queryKeys.ts @@ -0,0 +1,2 @@ +const PREFIX = 'reconstructor' +export const RECONSTRUTOR_SOLVE_QUERY_KEY = PREFIX diff --git a/src/features/solveContest/api/getSolveContestState.ts b/src/features/solveContest/api/getSolveContestState.ts index a9e996e0..ed0e560f 100644 --- a/src/features/solveContest/api/getSolveContestState.ts +++ b/src/features/solveContest/api/getSolveContestState.ts @@ -3,8 +3,7 @@ import { Discipline } from '@/types' import { queryOptions } from '@tanstack/react-query' import { SolveContestStateDTO } from '../types' import { API_ROUTE } from './constants' - -export const SOLVE_CONTEST_STATE_QUERY_KEY = 'solveContestState' +import { SOLVE_CONTEST_STATE_QUERY_KEY } from '../queryKeys' export const solveContestStateQuery = (contestNumber: number, discipline: Discipline) => queryOptions({ diff --git a/src/features/solveContest/queryKeys.ts b/src/features/solveContest/queryKeys.ts new file mode 100644 index 00000000..ef7bef38 --- /dev/null +++ b/src/features/solveContest/queryKeys.ts @@ -0,0 +1,2 @@ +const PREFIX = 'solveContest-' +export const SOLVE_CONTEST_STATE_QUERY_KEY = PREFIX + 'state' From d9edbfb026d3a8646898bf5162a6d38f5dcc85ae Mon Sep 17 00:00:00 2001 From: bohdancho Date: Sun, 24 Dec 2023 16:49:43 +0100 Subject: [PATCH 17/27] refactor: remove queryKeys files --- src/components/DevResetSession.tsx | 5 ++--- src/features/auth/api/getUser.ts | 2 +- src/features/auth/index.ts | 4 +--- src/features/auth/queryKeys.ts | 4 +--- src/features/contests/api/getContestResults.ts | 3 +-- src/features/contests/api/getOngoingContestNumber.ts | 3 +-- src/features/contests/index.ts | 1 - src/features/contests/queryKeys.ts | 4 ---- src/features/dashboard/api/getDashboard.ts | 3 +-- src/features/dashboard/index.ts | 3 +-- src/features/dashboard/queryKeys.ts | 2 -- src/features/leaderboard/api/getLeaderboard.ts | 3 +-- src/features/leaderboard/index.ts | 3 +-- src/features/leaderboard/queryKeys.ts | 2 -- src/features/reconstructor/api/getReconstruction.ts | 3 +-- src/features/reconstructor/index.ts | 1 - src/features/reconstructor/queryKeys.ts | 2 -- src/features/solveContest/api/getSolveContestState.ts | 4 ++-- src/features/solveContest/queryKeys.ts | 2 -- 19 files changed, 14 insertions(+), 40 deletions(-) delete mode 100644 src/features/contests/queryKeys.ts delete mode 100644 src/features/dashboard/queryKeys.ts delete mode 100644 src/features/leaderboard/queryKeys.ts delete mode 100644 src/features/reconstructor/queryKeys.ts delete mode 100644 src/features/solveContest/queryKeys.ts diff --git a/src/components/DevResetSession.tsx b/src/components/DevResetSession.tsx index 3bcee894..7b081ceb 100644 --- a/src/components/DevResetSession.tsx +++ b/src/components/DevResetSession.tsx @@ -1,4 +1,4 @@ -import { CONTEST_RESULTS_QUERY_KEY } from '@/features/contests' +import { USER_QUERY_KEY } from '@/features/auth' import { axiosClient } from '@/lib/axios' import { useQueryClient } from '@tanstack/react-query' import { useNavigate } from '@tanstack/react-router' @@ -9,8 +9,7 @@ export function DevResetSession() { const resetSession = async () => { try { await axiosClient.delete('/contests/round-session/') - queryClient.refetchQueries({ queryKey: [CONTEST_RESULTS_QUERY_KEY] }) - queryClient.refetchQueries({ queryKey: [SOLVE_CONTEST_STATE_QUERY_KEY] }) + queryClient.refetchQueries({ queryKey: [USER_QUERY_KEY] }) navigate({ to: '/contest' }) } catch (err) { alert("either you don't have any results to reset or something went wrong") diff --git a/src/features/auth/api/getUser.ts b/src/features/auth/api/getUser.ts index 8539e28e..a55bf91a 100644 --- a/src/features/auth/api/getUser.ts +++ b/src/features/auth/api/getUser.ts @@ -1,6 +1,5 @@ import { axiosClient } from '@/lib/axios' import { queryOptions } from '@tanstack/react-query' -import { USER_QUERY_KEY } from '../queryKeys' type UserData = { username: string; authCompleted: boolean } @@ -13,6 +12,7 @@ async function getUser() { } } +export const USER_QUERY_KEY = 'user' export const userQuery = queryOptions({ queryKey: [USER_QUERY_KEY], queryFn: getUser, diff --git a/src/features/auth/index.ts b/src/features/auth/index.ts index 2e12c845..20f5b127 100644 --- a/src/features/auth/index.ts +++ b/src/features/auth/index.ts @@ -1,11 +1,9 @@ import { queryClient } from '@/lib/reactQuery' -import { postLogin } from './api' +import { USER_QUERY_KEY, postLogin } from './api' import { setAuthTokens, deleteAuthTokens } from './authTokens' -import { USER_QUERY_KEY } from './queryKeys' export { createAuthorizedRequestInterceptor } from './authTokens' export * from './api' -export * from './queryKeys' export async function login(googleCode: string) { const response = await postLogin(googleCode) diff --git a/src/features/auth/queryKeys.ts b/src/features/auth/queryKeys.ts index 26b10b24..a854bf36 100644 --- a/src/features/auth/queryKeys.ts +++ b/src/features/auth/queryKeys.ts @@ -1,3 +1 @@ -const PREFIX = 'auth-' - -export const USER_QUERY_KEY = PREFIX + 'user' +export const USER_QUERY_KEY = 'user' as const diff --git a/src/features/contests/api/getContestResults.ts b/src/features/contests/api/getContestResults.ts index 6a815f67..cdff273b 100644 --- a/src/features/contests/api/getContestResults.ts +++ b/src/features/contests/api/getContestResults.ts @@ -1,7 +1,6 @@ import { axiosClient } from '@/lib/axios' import { Discipline, Scramble } from '@/types' import { queryOptions } from '@tanstack/react-query' -import { CONTEST_RESULTS_QUERY_KEY } from '../queryKeys' import { USER_QUERY_KEY } from '@/features/auth' export type ContestResultsDTO = Array<{ @@ -25,7 +24,7 @@ async function getContestResults(contestNumber: number, discipline: Discipline) export const contestResultsQuery = (contestNumber: number, discipline: Discipline) => queryOptions({ - queryKey: [USER_QUERY_KEY, CONTEST_RESULTS_QUERY_KEY, { contestNumber, discipline }], + queryKey: [USER_QUERY_KEY, 'contest-results', { contestNumber, discipline }], queryFn: () => { return getContestResults(contestNumber, discipline) }, diff --git a/src/features/contests/api/getOngoingContestNumber.ts b/src/features/contests/api/getOngoingContestNumber.ts index 57f8b56d..7de0ed31 100644 --- a/src/features/contests/api/getOngoingContestNumber.ts +++ b/src/features/contests/api/getOngoingContestNumber.ts @@ -1,6 +1,5 @@ import { axiosClient } from '@/lib/axios' import { queryOptions } from '@tanstack/react-query' -import { ONGOING_CONTEST_NUMBER_QUERY_KEY } from '../queryKeys' async function getOngoingContestNumber() { const res = await axiosClient.get('/contests/ongoing-contest-number/') @@ -8,6 +7,6 @@ async function getOngoingContestNumber() { } export const ongoingContestNumberQuery = queryOptions({ - queryKey: [ONGOING_CONTEST_NUMBER_QUERY_KEY], + queryKey: ['ongoing-contest-number'], queryFn: () => getOngoingContestNumber(), }) diff --git a/src/features/contests/index.ts b/src/features/contests/index.ts index 8022c6bf..aad1f00b 100644 --- a/src/features/contests/index.ts +++ b/src/features/contests/index.ts @@ -1,3 +1,2 @@ export { contestsRoute } from './routes' export { ongoingContestNumberQuery } from './api' -export * from './queryKeys' diff --git a/src/features/contests/queryKeys.ts b/src/features/contests/queryKeys.ts deleted file mode 100644 index 9b39fda0..00000000 --- a/src/features/contests/queryKeys.ts +++ /dev/null @@ -1,4 +0,0 @@ -const PREFIX = 'contests-' - -export const ONGOING_CONTEST_NUMBER_QUERY_KEY = PREFIX + 'ongoing-contest-number' -export const CONTEST_RESULTS_QUERY_KEY = 'contest-results' diff --git a/src/features/dashboard/api/getDashboard.ts b/src/features/dashboard/api/getDashboard.ts index f34ae13c..14169a20 100644 --- a/src/features/dashboard/api/getDashboard.ts +++ b/src/features/dashboard/api/getDashboard.ts @@ -1,7 +1,6 @@ import { axiosClient } from '@/lib/axios' import { Discipline } from '@/types' import { queryOptions } from '@tanstack/react-query' -import { DASHBOARD_QUERY_KEY } from '../queryKeys' export type DashboardDTO = { bestSolves: Array<{ @@ -25,4 +24,4 @@ async function getDashboard() { return res.data } -export const dashboardQuery = queryOptions({ queryKey: [DASHBOARD_QUERY_KEY], queryFn: getDashboard }) +export const dashboardQuery = queryOptions({ queryKey: ['dashboard'], queryFn: getDashboard }) diff --git a/src/features/dashboard/index.ts b/src/features/dashboard/index.ts index 950119a5..e1ee001c 100644 --- a/src/features/dashboard/index.ts +++ b/src/features/dashboard/index.ts @@ -1,2 +1 @@ -export * from './routes' -export * from './queryKeys' +export { dashboardRoute } from './routes' diff --git a/src/features/dashboard/queryKeys.ts b/src/features/dashboard/queryKeys.ts deleted file mode 100644 index c1b5e80e..00000000 --- a/src/features/dashboard/queryKeys.ts +++ /dev/null @@ -1,2 +0,0 @@ -const PREFIX = 'dashboard' -export const DASHBOARD_QUERY_KEY = PREFIX diff --git a/src/features/leaderboard/api/getLeaderboard.ts b/src/features/leaderboard/api/getLeaderboard.ts index 6de02b94..9b1a8db7 100644 --- a/src/features/leaderboard/api/getLeaderboard.ts +++ b/src/features/leaderboard/api/getLeaderboard.ts @@ -1,7 +1,6 @@ import { axiosClient } from '@/lib/axios' import { Discipline, Scramble } from '@/types' import { queryOptions } from '@tanstack/react-query' -import { LEADERBOARD_QUERY_KEY } from '../queryKeys' export type LeaderboardDTO = Array<{ id: number @@ -20,7 +19,7 @@ async function getLeaderboard(discipline: Discipline) { export const leaderboardQuery = (discipline: Discipline) => queryOptions({ - queryKey: [LEADERBOARD_QUERY_KEY, { discipline }], + queryKey: ['leaderboard', { discipline }], queryFn: () => getLeaderboard(discipline), retry: false, }) diff --git a/src/features/leaderboard/index.ts b/src/features/leaderboard/index.ts index 950119a5..6cfedcbc 100644 --- a/src/features/leaderboard/index.ts +++ b/src/features/leaderboard/index.ts @@ -1,2 +1 @@ -export * from './routes' -export * from './queryKeys' +export { leaderboardRoute } from './routes' diff --git a/src/features/leaderboard/queryKeys.ts b/src/features/leaderboard/queryKeys.ts deleted file mode 100644 index 72abd0fa..00000000 --- a/src/features/leaderboard/queryKeys.ts +++ /dev/null @@ -1,2 +0,0 @@ -const PREFIX = 'leaderboard' -export const LEADERBOARD_QUERY_KEY = PREFIX diff --git a/src/features/reconstructor/api/getReconstruction.ts b/src/features/reconstructor/api/getReconstruction.ts index 1cd967b8..efd93049 100644 --- a/src/features/reconstructor/api/getReconstruction.ts +++ b/src/features/reconstructor/api/getReconstruction.ts @@ -1,7 +1,6 @@ import { axiosClient } from '@/lib/axios' import { Discipline, Scramble } from '@/types' import { queryOptions } from '@tanstack/react-query' -import { RECONSTRUTOR_SOLVE_QUERY_KEY } from '../queryKeys' export type ReconstructionDTO = { id: number @@ -19,7 +18,7 @@ async function getReconstruction(solveId: number) { export const reconstructionQuery = (solveId: number | null) => queryOptions({ - queryKey: [RECONSTRUTOR_SOLVE_QUERY_KEY, solveId], + queryKey: ['reconstructor', solveId], queryFn: () => getReconstruction(solveId as number), enabled: typeof solveId === 'number', }) diff --git a/src/features/reconstructor/index.ts b/src/features/reconstructor/index.ts index ff16b14e..25182d36 100644 --- a/src/features/reconstructor/index.ts +++ b/src/features/reconstructor/index.ts @@ -1,7 +1,6 @@ import { useContext } from 'react' import { ReconstructorContext } from './components/ReconstructorProvider' -export * from './queryKeys' export { ReconstructorProvider } from './components' export function useReconstructor() { return useContext(ReconstructorContext) diff --git a/src/features/reconstructor/queryKeys.ts b/src/features/reconstructor/queryKeys.ts deleted file mode 100644 index b25104e2..00000000 --- a/src/features/reconstructor/queryKeys.ts +++ /dev/null @@ -1,2 +0,0 @@ -const PREFIX = 'reconstructor' -export const RECONSTRUTOR_SOLVE_QUERY_KEY = PREFIX diff --git a/src/features/solveContest/api/getSolveContestState.ts b/src/features/solveContest/api/getSolveContestState.ts index ed0e560f..8dca2575 100644 --- a/src/features/solveContest/api/getSolveContestState.ts +++ b/src/features/solveContest/api/getSolveContestState.ts @@ -3,11 +3,11 @@ import { Discipline } from '@/types' import { queryOptions } from '@tanstack/react-query' import { SolveContestStateDTO } from '../types' import { API_ROUTE } from './constants' -import { SOLVE_CONTEST_STATE_QUERY_KEY } from '../queryKeys' +import { USER_QUERY_KEY } from '@/features/auth' export const solveContestStateQuery = (contestNumber: number, discipline: Discipline) => queryOptions({ - queryKey: [SOLVE_CONTEST_STATE_QUERY_KEY, { contestNumber, discipline }], + queryKey: [USER_QUERY_KEY, 'solve-contest-state', { contestNumber, discipline }], queryFn: async () => { const res = await axiosClient.get(`${API_ROUTE}${contestNumber}/${discipline}/`) return res.data diff --git a/src/features/solveContest/queryKeys.ts b/src/features/solveContest/queryKeys.ts deleted file mode 100644 index ef7bef38..00000000 --- a/src/features/solveContest/queryKeys.ts +++ /dev/null @@ -1,2 +0,0 @@ -const PREFIX = 'solveContest-' -export const SOLVE_CONTEST_STATE_QUERY_KEY = PREFIX + 'state' From 3b7d3ad59bda9032f4a59733593b5c0f17693f56 Mon Sep 17 00:00:00 2001 From: bohdancho Date: Sun, 24 Dec 2023 20:14:12 +0100 Subject: [PATCH 18/27] fix: delete leftover file --- src/features/auth/queryKeys.ts | 1 - 1 file changed, 1 deletion(-) delete mode 100644 src/features/auth/queryKeys.ts diff --git a/src/features/auth/queryKeys.ts b/src/features/auth/queryKeys.ts deleted file mode 100644 index a854bf36..00000000 --- a/src/features/auth/queryKeys.ts +++ /dev/null @@ -1 +0,0 @@ -export const USER_QUERY_KEY = 'user' as const From 1fb25b2c71f7e3930f39717bdf036f1fe1cdf85b Mon Sep 17 00:00:00 2001 From: bohdancho Date: Sun, 24 Dec 2023 20:14:32 +0100 Subject: [PATCH 19/27] fix: axios interceptor refetch auth token only if has tokens --- src/lib/axios.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/lib/axios.ts b/src/lib/axios.ts index daf25424..176d8b60 100644 --- a/src/lib/axios.ts +++ b/src/lib/axios.ts @@ -1,4 +1,5 @@ import { createAuthorizedRequestInterceptor, refreshAccessToken } from '@/features/auth' +import { getAuthTokens } from '@/features/auth/authTokens' import axios from 'axios' import createAuthRefreshInterceptor from 'axios-auth-refresh' import applyCaseMiddleware from 'axios-case-converter' @@ -14,6 +15,8 @@ const _axiosClient = axios.create(axiosParams) applyCaseMiddleware(_axiosClient) createAuthorizedRequestInterceptor(_axiosClient) -createAuthRefreshInterceptor(_axiosClient, () => refreshAccessToken(axiosParams)) +createAuthRefreshInterceptor(_axiosClient, () => refreshAccessToken(axiosParams), { + shouldRefresh: (err) => err.response?.status === 401 && !!getAuthTokens(), +}) export const axiosClient = _axiosClient From e723d1f4a8c3bcbe46f712634557ec1e0d438df4 Mon Sep 17 00:00:00 2001 From: bohdancho Date: Sun, 24 Dec 2023 20:15:13 +0100 Subject: [PATCH 20/27] fix: type naming error --- src/features/contests/components/PublishedSession.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/features/contests/components/PublishedSession.tsx b/src/features/contests/components/PublishedSession.tsx index b71282d4..400a3d7f 100644 --- a/src/features/contests/components/PublishedSession.tsx +++ b/src/features/contests/components/PublishedSession.tsx @@ -1,10 +1,10 @@ import { ReconstructTimeButton, ResultCard } from '@/components' -import { ContestResultsResponse } from '@/api/contests' import { cn, formatTimeResult } from '@/utils' import { useMemo } from 'react' import { useReconstructor } from '@/features/reconstructor' +import { ContestResultsDTO } from '../api' -type PublishedSessionProps = ContestResultsResponse[number] +type PublishedSessionProps = ContestResultsDTO[number] export function PublishedSession({ user: { username }, avgMs, From ca99e9a99806f22da6fae98a1dc64fdad2a89ed9 Mon Sep 17 00:00:00 2001 From: bohdancho Date: Sun, 24 Dec 2023 20:16:41 +0100 Subject: [PATCH 21/27] fix: ContestDiscipline reduce unnecessary fetching --- .../contests/components/ContestDiscipline.tsx | 12 ++++++------ src/features/contests/routes/index.tsx | 6 ++++-- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/features/contests/components/ContestDiscipline.tsx b/src/features/contests/components/ContestDiscipline.tsx index d5680b4c..6df6bb3e 100644 --- a/src/features/contests/components/ContestDiscipline.tsx +++ b/src/features/contests/components/ContestDiscipline.tsx @@ -11,10 +11,14 @@ export function ContestDiscipline() { const { data: userData } = useQuery(userQuery) const routeParams = contestDisciplineRoute.useParams() const query = contestDisciplineRoute.useLoaderData() - const { data: sessions, error, isLoading } = useQuery(query) + const { data: sessions, error, isLoading } = useQuery({ ...query, enabled: !!userData }) if (isLoading) { - return loading... + return Loading... + } + + if (userData === null) { + return You need to be signed in to participate in a contest } if (error?.response?.status === 403) { @@ -26,10 +30,6 @@ export function ContestDiscipline() { ) } - if (error?.response?.status === 401) { - return You need to be signed in to participate in a contest - } - if (error || sessions === undefined) { return An unknown error occured } diff --git a/src/features/contests/routes/index.tsx b/src/features/contests/routes/index.tsx index 5aab5829..ed1e6660 100644 --- a/src/features/contests/routes/index.tsx +++ b/src/features/contests/routes/index.tsx @@ -4,6 +4,7 @@ import { DEFAULT_DISCIPLINE, isDiscipline } from '@/types' import { Route } from '@tanstack/react-router' import { ContestDiscipline } from '../components' import { ongoingContestNumberQuery, contestResultsQuery } from '../api' +import { userQuery } from '@/features/auth' const allContestsRoute = new Route({ getParentRoute: () => rootRoute, path: '/contest' }) const allContestsIndexRoute = new Route({ @@ -32,7 +33,7 @@ const contestIndexRoute = new Route({ export const contestDisciplineRoute = new Route({ getParentRoute: () => contestRoute, path: '$discipline', - loader: ({ params, navigate, context: { queryClient } }) => { + loader: async ({ params, navigate, context: { queryClient } }) => { const contestNumber = Number(params.contestNumber) if (isNaN(contestNumber)) { throw navigate({ to: '../../', replace: true }) @@ -41,8 +42,9 @@ export const contestDisciplineRoute = new Route({ throw navigate({ to: '../', replace: true }) } + const userData = await queryClient.fetchQuery(userQuery) const query = contestResultsQuery(contestNumber, params.discipline) - queryClient.ensureQueryData(query) + if (userData) queryClient.ensureQueryData(query) return query }, component: () => ( From bb6adc691a04427da4a9df0a3ef380820eded09c Mon Sep 17 00:00:00 2001 From: bohdancho Date: Mon, 25 Dec 2023 08:26:03 +0100 Subject: [PATCH 22/27] Revert "fix: ContestDiscipline reduce unnecessary fetching" This reverts commit ca99e9a99806f22da6fae98a1dc64fdad2a89ed9. --- .../contests/components/ContestDiscipline.tsx | 12 ++++++------ src/features/contests/routes/index.tsx | 6 ++---- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/features/contests/components/ContestDiscipline.tsx b/src/features/contests/components/ContestDiscipline.tsx index 6df6bb3e..d5680b4c 100644 --- a/src/features/contests/components/ContestDiscipline.tsx +++ b/src/features/contests/components/ContestDiscipline.tsx @@ -11,14 +11,10 @@ export function ContestDiscipline() { const { data: userData } = useQuery(userQuery) const routeParams = contestDisciplineRoute.useParams() const query = contestDisciplineRoute.useLoaderData() - const { data: sessions, error, isLoading } = useQuery({ ...query, enabled: !!userData }) + const { data: sessions, error, isLoading } = useQuery(query) if (isLoading) { - return Loading... - } - - if (userData === null) { - return You need to be signed in to participate in a contest + return loading... } if (error?.response?.status === 403) { @@ -30,6 +26,10 @@ export function ContestDiscipline() { ) } + if (error?.response?.status === 401) { + return You need to be signed in to participate in a contest + } + if (error || sessions === undefined) { return An unknown error occured } diff --git a/src/features/contests/routes/index.tsx b/src/features/contests/routes/index.tsx index ed1e6660..5aab5829 100644 --- a/src/features/contests/routes/index.tsx +++ b/src/features/contests/routes/index.tsx @@ -4,7 +4,6 @@ import { DEFAULT_DISCIPLINE, isDiscipline } from '@/types' import { Route } from '@tanstack/react-router' import { ContestDiscipline } from '../components' import { ongoingContestNumberQuery, contestResultsQuery } from '../api' -import { userQuery } from '@/features/auth' const allContestsRoute = new Route({ getParentRoute: () => rootRoute, path: '/contest' }) const allContestsIndexRoute = new Route({ @@ -33,7 +32,7 @@ const contestIndexRoute = new Route({ export const contestDisciplineRoute = new Route({ getParentRoute: () => contestRoute, path: '$discipline', - loader: async ({ params, navigate, context: { queryClient } }) => { + loader: ({ params, navigate, context: { queryClient } }) => { const contestNumber = Number(params.contestNumber) if (isNaN(contestNumber)) { throw navigate({ to: '../../', replace: true }) @@ -42,9 +41,8 @@ export const contestDisciplineRoute = new Route({ throw navigate({ to: '../', replace: true }) } - const userData = await queryClient.fetchQuery(userQuery) const query = contestResultsQuery(contestNumber, params.discipline) - if (userData) queryClient.ensureQueryData(query) + queryClient.ensureQueryData(query) return query }, component: () => ( From efb04c8576b668ee9ef8c7a68d2d56c8d7b17095 Mon Sep 17 00:00:00 2001 From: bohdancho Date: Mon, 25 Dec 2023 08:28:52 +0100 Subject: [PATCH 23/27] fix: outdated imports after structure refactor --- src/features/contests/components/PublishedSession.tsx | 2 +- src/features/dashboard/components/ContestsList.tsx | 2 +- src/features/solveContest/SolveContest.tsx | 2 +- src/features/solveContest/components/SubmittedSolve.tsx | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/features/contests/components/PublishedSession.tsx b/src/features/contests/components/PublishedSession.tsx index 400a3d7f..df318632 100644 --- a/src/features/contests/components/PublishedSession.tsx +++ b/src/features/contests/components/PublishedSession.tsx @@ -58,7 +58,7 @@ export function PublishedSession({ ) } -function getBestAndWorstIds(submittedSolves: ContestResultsResponse[number]['solveSet']) { +function getBestAndWorstIds(submittedSolves: ContestResultsDTO[number]['solveSet']) { const dnfSolve = submittedSolves.find(({ dnf }) => dnf) const timeArr = submittedSolves .filter(({ timeMs, dnf }) => typeof timeMs === 'number' && !dnf) diff --git a/src/features/dashboard/components/ContestsList.tsx b/src/features/dashboard/components/ContestsList.tsx index 32df49ed..4a6b4bbb 100644 --- a/src/features/dashboard/components/ContestsList.tsx +++ b/src/features/dashboard/components/ContestsList.tsx @@ -1,7 +1,7 @@ import { DEFAULT_DISCIPLINE } from '@/types' import { cn } from '@/utils' import { Link } from '@tanstack/react-router' -import { DashboardDTO } from '../api/dashboard' +import { DashboardDTO } from '../api' type ContestsListProps = { contests?: DashboardDTO['contests'] } export function ContestsList({ contests }: ContestsListProps) { diff --git a/src/features/solveContest/SolveContest.tsx b/src/features/solveContest/SolveContest.tsx index 0f8a21bc..270266c0 100644 --- a/src/features/solveContest/SolveContest.tsx +++ b/src/features/solveContest/SolveContest.tsx @@ -1,10 +1,10 @@ import { Discipline } from '@/types' import { CurrentSolve, SubmittedSolve } from './components' import { InfoBox } from '@/components' -import { CubeSolveResult } from '@/integrations/cube' import { useQuery } from '@tanstack/react-query' import { solveContestStateQuery, usePostSolveResult, useSubmitSolve, useChangeToExtra } from './api' import { userQuery } from '../auth' +import { CubeSolveResult } from '../cube' type SolveContestProps = { contestNumber: number; discipline: Discipline } export function SolveContest({ contestNumber, discipline }: SolveContestProps) { diff --git a/src/features/solveContest/components/SubmittedSolve.tsx b/src/features/solveContest/components/SubmittedSolve.tsx index 97256b5d..1b50445e 100644 --- a/src/features/solveContest/components/SubmittedSolve.tsx +++ b/src/features/solveContest/components/SubmittedSolve.tsx @@ -1,9 +1,9 @@ -import { SolveContestStateResponse } from '@/api/contests' import { ReconstructTimeButton } from '@/components' import { useReconstructor } from '@/features/reconstructor' import { cn } from '@/utils' +import { SolveContestStateDTO } from '../types' -type SubmittedSolveProps = SolveContestStateResponse['submittedSolves'][number] & { +type SubmittedSolveProps = SolveContestStateDTO['submittedSolves'][number] & { className?: string } export function SubmittedSolve({ className, timeMs, scramble, id }: SubmittedSolveProps) { From 3a7fbeae28b69205a9cb6f38a1ea89848c2ad99b Mon Sep 17 00:00:00 2001 From: bohdancho Date: Mon, 25 Dec 2023 09:05:40 +0100 Subject: [PATCH 24/27] infra: update eslint config --- .eslintrc.cjs | 12 +++++++++++- src/components/DisciplinesTabsLayout.tsx | 2 +- src/components/layout/components/Navbar.tsx | 2 +- src/components/ui/InfoBox.tsx | 2 +- src/components/ui/ReconstructTimeButton.tsx | 2 +- src/components/ui/ResultCard.tsx | 4 ++-- src/features/auth/api/refreshAccessToken.ts | 2 +- src/features/auth/authTokens.ts | 2 +- src/features/contests/api/getContestResults.ts | 2 +- .../contests/components/ContestDiscipline.tsx | 4 ++-- .../contests/components/PublishedSession.tsx | 2 +- src/features/cube/components/Cube.tsx | 2 +- src/features/cube/components/CubeProvider.tsx | 4 ++-- src/features/dashboard/api/getDashboard.ts | 2 +- src/features/dashboard/components/BestSolves.tsx | 2 +- src/features/dashboard/components/ContestsList.tsx | 2 +- src/features/leaderboard/api/getLeaderboard.ts | 2 +- .../leaderboard/components/LeaderboardResult.tsx | 2 +- src/features/reconstructor/api/getReconstruction.ts | 2 +- .../components/ReconstructorProvider.tsx | 4 ++-- src/features/solveContest/SolveContest.tsx | 4 ++-- src/features/solveContest/api/changeToExtra.ts | 4 ++-- .../solveContest/api/getSolveContestState.ts | 4 ++-- src/features/solveContest/api/postSolveResult.ts | 2 +- src/features/solveContest/api/submitSolve.ts | 4 ++-- .../solveContest/components/CurrentSolve.tsx | 4 ++-- .../solveContest/components/SubmittedSolve.tsx | 2 +- src/features/solveContest/types/index.ts | 2 +- src/lib/reactQuery.ts | 2 +- src/router.tsx | 2 +- src/utils/cn.ts | 2 +- 31 files changed, 50 insertions(+), 40 deletions(-) diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 2124d989..b155980f 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -10,7 +10,17 @@ const config = { ], ignorePatterns: ['dist', 'node_modules', '!.*.*'], parser: '@typescript-eslint/parser', - plugins: ['react-refresh'], + plugins: ['react-refresh', '@typescript-eslint'], + rules: { + '@typescript-eslint/consistent-type-imports': [ + 'warn', + { + prefer: 'type-imports', + fixStyle: 'inline-type-imports', + }, + ], + '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }], + }, } module.exports = config diff --git a/src/components/DisciplinesTabsLayout.tsx b/src/components/DisciplinesTabsLayout.tsx index 7c5ad49a..026fc9df 100644 --- a/src/components/DisciplinesTabsLayout.tsx +++ b/src/components/DisciplinesTabsLayout.tsx @@ -1,5 +1,5 @@ import CubeIcon from '@/assets/3by3.svg?react' -import { ReactNode } from 'react' +import { type ReactNode } from 'react' export function DisciplinesTabsLayout({ children }: { children: ReactNode }) { return ( diff --git a/src/components/layout/components/Navbar.tsx b/src/components/layout/components/Navbar.tsx index 685c35d3..0a874201 100644 --- a/src/components/layout/components/Navbar.tsx +++ b/src/components/layout/components/Navbar.tsx @@ -2,7 +2,7 @@ import { ongoingContestNumberQuery } from '@/features/contests' import { cn } from '@/utils' import { useQuery } from '@tanstack/react-query' import { Link, useParams } from '@tanstack/react-router' -import { ButtonHTMLAttributes, useMemo, useState } from 'react' +import { type ButtonHTMLAttributes, useMemo, useState } from 'react' export function NavBar() { const { data: ongoingContestNumber } = useQuery(ongoingContestNumberQuery) diff --git a/src/components/ui/InfoBox.tsx b/src/components/ui/InfoBox.tsx index 9834596a..44a32138 100644 --- a/src/components/ui/InfoBox.tsx +++ b/src/components/ui/InfoBox.tsx @@ -1,4 +1,4 @@ -import { ReactNode } from 'react' +import { type ReactNode } from 'react' export function InfoBox({ children }: { children: ReactNode }) { return ( diff --git a/src/components/ui/ReconstructTimeButton.tsx b/src/components/ui/ReconstructTimeButton.tsx index 1ce0d2fb..bc59dd74 100644 --- a/src/components/ui/ReconstructTimeButton.tsx +++ b/src/components/ui/ReconstructTimeButton.tsx @@ -1,5 +1,5 @@ import { cn, formatTimeResult } from '@/utils' -import { ButtonHTMLAttributes } from 'react' +import { type ButtonHTMLAttributes } from 'react' type SolveTimeButtonProps = { timeMs: number diff --git a/src/components/ui/ResultCard.tsx b/src/components/ui/ResultCard.tsx index cfe7d8d5..0c017a03 100644 --- a/src/components/ui/ResultCard.tsx +++ b/src/components/ui/ResultCard.tsx @@ -1,6 +1,6 @@ import { cn } from '@/utils' -import { VariantProps, cva } from 'class-variance-authority' -import { HTMLAttributes, forwardRef } from 'react' +import { type VariantProps, cva } from 'class-variance-authority' +import { type HTMLAttributes, forwardRef } from 'react' const variants = cva('rounded-md px-1 pb-2 pt-2 text-[13px] md:py-3 md:px-3 md:text-base lg:px-8', { variants: { diff --git a/src/features/auth/api/refreshAccessToken.ts b/src/features/auth/api/refreshAccessToken.ts index 473de879..cb994d41 100644 --- a/src/features/auth/api/refreshAccessToken.ts +++ b/src/features/auth/api/refreshAccessToken.ts @@ -1,4 +1,4 @@ -import axios, { AxiosRequestConfig } from 'axios' +import axios, { type AxiosRequestConfig } from 'axios' import { getAuthTokens, setAuthTokens, deleteAuthTokens } from '../authTokens' export async function refreshAccessToken(axiosParams: AxiosRequestConfig) { diff --git a/src/features/auth/authTokens.ts b/src/features/auth/authTokens.ts index e34f7c29..59afa0d1 100644 --- a/src/features/auth/authTokens.ts +++ b/src/features/auth/authTokens.ts @@ -1,4 +1,4 @@ -import { AxiosInstance } from 'axios' +import { type AxiosInstance } from 'axios' const LS_REFRESH_TOKEN = 'refresh-token' const LS_ACCESS_TOKEN = 'access-token' diff --git a/src/features/contests/api/getContestResults.ts b/src/features/contests/api/getContestResults.ts index cdff273b..770fac42 100644 --- a/src/features/contests/api/getContestResults.ts +++ b/src/features/contests/api/getContestResults.ts @@ -1,5 +1,5 @@ import { axiosClient } from '@/lib/axios' -import { Discipline, Scramble } from '@/types' +import { type Discipline, type Scramble } from '@/types' import { queryOptions } from '@tanstack/react-query' import { USER_QUERY_KEY } from '@/features/auth' diff --git a/src/features/contests/components/ContestDiscipline.tsx b/src/features/contests/components/ContestDiscipline.tsx index d5680b4c..549b8bc5 100644 --- a/src/features/contests/components/ContestDiscipline.tsx +++ b/src/features/contests/components/ContestDiscipline.tsx @@ -1,10 +1,10 @@ import { InfoBox } from '@/components' import { useQuery } from '@tanstack/react-query' -import { Discipline } from '@/types' +import { type Discipline } from '@/types' import { SolveContest } from '@/features/solveContest' import { contestDisciplineRoute } from '../routes' import { PublishedSession } from './PublishedSession' -import { ContestResultsDTO } from '../api' +import { type ContestResultsDTO } from '../api' import { userQuery } from '@/features/auth' export function ContestDiscipline() { diff --git a/src/features/contests/components/PublishedSession.tsx b/src/features/contests/components/PublishedSession.tsx index df318632..14a11ddb 100644 --- a/src/features/contests/components/PublishedSession.tsx +++ b/src/features/contests/components/PublishedSession.tsx @@ -2,7 +2,7 @@ import { ReconstructTimeButton, ResultCard } from '@/components' import { cn, formatTimeResult } from '@/utils' import { useMemo } from 'react' import { useReconstructor } from '@/features/reconstructor' -import { ContestResultsDTO } from '../api' +import { type ContestResultsDTO } from '../api' type PublishedSessionProps = ContestResultsDTO[number] export function PublishedSession({ diff --git a/src/features/cube/components/Cube.tsx b/src/features/cube/components/Cube.tsx index f447db6e..eff1720c 100644 --- a/src/features/cube/components/Cube.tsx +++ b/src/features/cube/components/Cube.tsx @@ -1,4 +1,4 @@ -import { RefObject, useEffect, useState } from 'react' +import { type RefObject, useEffect, useState } from 'react' export type CubeSolveResult = | { reconstruction: string; timeMs: number; dnf: false } diff --git a/src/features/cube/components/CubeProvider.tsx b/src/features/cube/components/CubeProvider.tsx index 6ca83f0f..1c073ef8 100644 --- a/src/features/cube/components/CubeProvider.tsx +++ b/src/features/cube/components/CubeProvider.tsx @@ -1,8 +1,8 @@ import { createContext, useCallback, useMemo, useRef, useState } from 'react' import { cn, isTouchDevice, useConditionalBeforeUnload } from '@/utils' import { useLocalStorage } from 'usehooks-ts' -import { CubeSolveResult } from '..' -import { CubeSolveFinishCallback, Cube } from './Cube' +import { type CubeSolveResult } from '..' +import { type CubeSolveFinishCallback, Cube } from './Cube' import { AbortPrompt } from './AbortPrompt' import { DeviceWarningModal } from './DeviceWarningModal' diff --git a/src/features/dashboard/api/getDashboard.ts b/src/features/dashboard/api/getDashboard.ts index 14169a20..8640cc3c 100644 --- a/src/features/dashboard/api/getDashboard.ts +++ b/src/features/dashboard/api/getDashboard.ts @@ -1,5 +1,5 @@ import { axiosClient } from '@/lib/axios' -import { Discipline } from '@/types' +import { type Discipline } from '@/types' import { queryOptions } from '@tanstack/react-query' export type DashboardDTO = { diff --git a/src/features/dashboard/components/BestSolves.tsx b/src/features/dashboard/components/BestSolves.tsx index 8903f003..9cdec00f 100644 --- a/src/features/dashboard/components/BestSolves.tsx +++ b/src/features/dashboard/components/BestSolves.tsx @@ -1,7 +1,7 @@ import { ReconstructTimeButton } from '@/components' import CubeIcon from '@/assets/3by3.svg?react' import { Link } from '@tanstack/react-router' -import { DashboardDTO } from '../api/getDashboard' +import { type DashboardDTO } from '../api/getDashboard' import { useReconstructor } from '@/features/reconstructor' type BestSolvesProps = { bestSolves?: DashboardDTO['bestSolves'] } diff --git a/src/features/dashboard/components/ContestsList.tsx b/src/features/dashboard/components/ContestsList.tsx index 4a6b4bbb..f5096d67 100644 --- a/src/features/dashboard/components/ContestsList.tsx +++ b/src/features/dashboard/components/ContestsList.tsx @@ -1,7 +1,7 @@ import { DEFAULT_DISCIPLINE } from '@/types' import { cn } from '@/utils' import { Link } from '@tanstack/react-router' -import { DashboardDTO } from '../api' +import { type DashboardDTO } from '../api' type ContestsListProps = { contests?: DashboardDTO['contests'] } export function ContestsList({ contests }: ContestsListProps) { diff --git a/src/features/leaderboard/api/getLeaderboard.ts b/src/features/leaderboard/api/getLeaderboard.ts index 9b1a8db7..b48c3666 100644 --- a/src/features/leaderboard/api/getLeaderboard.ts +++ b/src/features/leaderboard/api/getLeaderboard.ts @@ -1,5 +1,5 @@ import { axiosClient } from '@/lib/axios' -import { Discipline, Scramble } from '@/types' +import { type Discipline, type Scramble } from '@/types' import { queryOptions } from '@tanstack/react-query' export type LeaderboardDTO = Array<{ diff --git a/src/features/leaderboard/components/LeaderboardResult.tsx b/src/features/leaderboard/components/LeaderboardResult.tsx index 6061d932..24450fc9 100644 --- a/src/features/leaderboard/components/LeaderboardResult.tsx +++ b/src/features/leaderboard/components/LeaderboardResult.tsx @@ -1,6 +1,6 @@ import { ResultCard, ReconstructTimeButton } from '@/components' import { Link } from '@tanstack/react-router' -import { LeaderboardDTO } from '../api' +import { type LeaderboardDTO } from '../api' import { useReconstructor } from '@/features/reconstructor' export function LeaderboardResult({ diff --git a/src/features/reconstructor/api/getReconstruction.ts b/src/features/reconstructor/api/getReconstruction.ts index efd93049..3af49cca 100644 --- a/src/features/reconstructor/api/getReconstruction.ts +++ b/src/features/reconstructor/api/getReconstruction.ts @@ -1,5 +1,5 @@ import { axiosClient } from '@/lib/axios' -import { Discipline, Scramble } from '@/types' +import { type Discipline, type Scramble } from '@/types' import { queryOptions } from '@tanstack/react-query' export type ReconstructionDTO = { diff --git a/src/features/reconstructor/components/ReconstructorProvider.tsx b/src/features/reconstructor/components/ReconstructorProvider.tsx index 0a9e85e1..e4d7194f 100644 --- a/src/features/reconstructor/components/ReconstructorProvider.tsx +++ b/src/features/reconstructor/components/ReconstructorProvider.tsx @@ -1,8 +1,8 @@ import { createContext, useCallback, useMemo, useState } from 'react' -import { Reconstruction, ReconstructionMetadata, Reconstructor } from './Reconstructor' +import { type Reconstruction, type ReconstructionMetadata, Reconstructor } from './Reconstructor' import { cn, formatTimeResult } from '@/utils' import { useQuery } from '@tanstack/react-query' -import { reconstructionQuery, ReconstructionDTO } from '../api' +import { reconstructionQuery, type ReconstructionDTO } from '../api' type ReconstructorContextValue = { showReconstruction: (solveId: number, onClose?: () => void) => void diff --git a/src/features/solveContest/SolveContest.tsx b/src/features/solveContest/SolveContest.tsx index 270266c0..b98ac580 100644 --- a/src/features/solveContest/SolveContest.tsx +++ b/src/features/solveContest/SolveContest.tsx @@ -1,10 +1,10 @@ -import { Discipline } from '@/types' +import { type Discipline } from '@/types' import { CurrentSolve, SubmittedSolve } from './components' import { InfoBox } from '@/components' import { useQuery } from '@tanstack/react-query' import { solveContestStateQuery, usePostSolveResult, useSubmitSolve, useChangeToExtra } from './api' import { userQuery } from '../auth' -import { CubeSolveResult } from '../cube' +import { type CubeSolveResult } from '../cube' type SolveContestProps = { contestNumber: number; discipline: Discipline } export function SolveContest({ contestNumber, discipline }: SolveContestProps) { diff --git a/src/features/solveContest/api/changeToExtra.ts b/src/features/solveContest/api/changeToExtra.ts index 72094949..fc8bc30b 100644 --- a/src/features/solveContest/api/changeToExtra.ts +++ b/src/features/solveContest/api/changeToExtra.ts @@ -1,8 +1,8 @@ import { axiosClient } from '@/lib/axios' import { queryClient } from '@/lib/reactQuery' -import { Discipline } from '@/types' +import { type Discipline } from '@/types' import { useMutation } from '@tanstack/react-query' -import { SolveContestStateDTO } from '../types' +import { type SolveContestStateDTO } from '../types' import { API_ROUTE } from './constants' import { solveContestStateQuery } from './getSolveContestState' diff --git a/src/features/solveContest/api/getSolveContestState.ts b/src/features/solveContest/api/getSolveContestState.ts index 8dca2575..e2c2b358 100644 --- a/src/features/solveContest/api/getSolveContestState.ts +++ b/src/features/solveContest/api/getSolveContestState.ts @@ -1,7 +1,7 @@ import { axiosClient } from '@/lib/axios' -import { Discipline } from '@/types' +import { type Discipline } from '@/types' import { queryOptions } from '@tanstack/react-query' -import { SolveContestStateDTO } from '../types' +import { type SolveContestStateDTO } from '../types' import { API_ROUTE } from './constants' import { USER_QUERY_KEY } from '@/features/auth' diff --git a/src/features/solveContest/api/postSolveResult.ts b/src/features/solveContest/api/postSolveResult.ts index 50d33386..ca27bdda 100644 --- a/src/features/solveContest/api/postSolveResult.ts +++ b/src/features/solveContest/api/postSolveResult.ts @@ -1,6 +1,6 @@ import { queryClient } from '@/lib/reactQuery' import { axiosClient } from '@/lib/axios' -import { Discipline } from '@/types' +import { type Discipline } from '@/types' import { useMutation } from '@tanstack/react-query' import { API_ROUTE } from './constants' import { solveContestStateQuery } from './getSolveContestState' diff --git a/src/features/solveContest/api/submitSolve.ts b/src/features/solveContest/api/submitSolve.ts index 6e620302..1e3b4d6d 100644 --- a/src/features/solveContest/api/submitSolve.ts +++ b/src/features/solveContest/api/submitSolve.ts @@ -1,8 +1,8 @@ import { axiosClient } from '@/lib/axios' import { queryClient } from '@/lib/reactQuery' -import { Discipline } from '@/types' +import { type Discipline } from '@/types' import { useMutation } from '@tanstack/react-query' -import { SolveContestStateDTO } from '../types' +import { type SolveContestStateDTO } from '../types' import { API_ROUTE } from './constants' import { solveContestStateQuery } from './getSolveContestState' import { contestResultsQuery } from '@/features/contests/api' diff --git a/src/features/solveContest/components/CurrentSolve.tsx b/src/features/solveContest/components/CurrentSolve.tsx index 4a7f7ed4..1bce56f5 100644 --- a/src/features/solveContest/components/CurrentSolve.tsx +++ b/src/features/solveContest/components/CurrentSolve.tsx @@ -1,8 +1,8 @@ import { ReconstructTimeButton } from '@/components' import { cn } from '@/utils' import { useReconstructor } from '@/features/reconstructor' -import { SolveContestStateDTO } from '../types' -import { CubeSolveResult, useCube } from '@/features/cube' +import { type SolveContestStateDTO } from '../types' +import { type CubeSolveResult, useCube } from '@/features/cube' type CurrentSolveProps = SolveContestStateDTO['currentSolve'] & { className?: string diff --git a/src/features/solveContest/components/SubmittedSolve.tsx b/src/features/solveContest/components/SubmittedSolve.tsx index 1b50445e..1128c2bd 100644 --- a/src/features/solveContest/components/SubmittedSolve.tsx +++ b/src/features/solveContest/components/SubmittedSolve.tsx @@ -1,7 +1,7 @@ import { ReconstructTimeButton } from '@/components' import { useReconstructor } from '@/features/reconstructor' import { cn } from '@/utils' -import { SolveContestStateDTO } from '../types' +import { type SolveContestStateDTO } from '../types' type SubmittedSolveProps = SolveContestStateDTO['submittedSolves'][number] & { className?: string diff --git a/src/features/solveContest/types/index.ts b/src/features/solveContest/types/index.ts index d94a8949..f655c81d 100644 --- a/src/features/solveContest/types/index.ts +++ b/src/features/solveContest/types/index.ts @@ -1,4 +1,4 @@ -import { Scramble } from '@/types' +import { type Scramble } from '@/types' export type SolveContestStateDTO = { currentSolve: { diff --git a/src/lib/reactQuery.ts b/src/lib/reactQuery.ts index c8fc84bf..b1d8e7c8 100644 --- a/src/lib/reactQuery.ts +++ b/src/lib/reactQuery.ts @@ -1,5 +1,5 @@ import { QueryClient } from '@tanstack/react-query' -import { AxiosError } from 'axios' +import { type AxiosError } from 'axios' export const queryClient = new QueryClient({ defaultOptions: { diff --git a/src/router.tsx b/src/router.tsx index e9914dc6..c0b9974c 100644 --- a/src/router.tsx +++ b/src/router.tsx @@ -3,7 +3,7 @@ import { TanStackRouterDevtools } from '@tanstack/router-devtools' import { Route, Router, rootRouteWithContext } from '@tanstack/react-router' import { leaderboardRoute } from './features/leaderboard' import { contestsRoute } from './features/contests' -import { QueryClient } from '@tanstack/react-query' +import { type QueryClient } from '@tanstack/react-query' import { ReactQueryDevtools } from '@tanstack/react-query-devtools' import { dashboardRoute } from './features/dashboard' import { queryClient } from './lib/reactQuery' diff --git a/src/utils/cn.ts b/src/utils/cn.ts index ef18d1fc..30777787 100644 --- a/src/utils/cn.ts +++ b/src/utils/cn.ts @@ -1,4 +1,4 @@ -import clsx, { ClassValue } from 'clsx' +import clsx, { type ClassValue } from 'clsx' import { twMerge } from 'tailwind-merge' export function cn(...inputs: ClassValue[]) { From b387a107ed0f750b155a39a6d0f6c99c729941ba Mon Sep 17 00:00:00 2001 From: bohdancho Date: Mon, 25 Dec 2023 10:49:37 +0100 Subject: [PATCH 25/27] refactor: make eslint config more strict --- .eslintrc.cjs | 13 ++++++++++++- tsconfig.json | 2 +- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/.eslintrc.cjs b/.eslintrc.cjs index b155980f..952e70f3 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -4,14 +4,19 @@ const config = { env: { browser: true, es2020: true, node: true }, extends: [ 'eslint:recommended', - 'plugin:@typescript-eslint/recommended', + 'plugin:@typescript-eslint/recommended-type-checked', + 'plugin:@typescript-eslint/stylistic-type-checked', 'plugin:react-hooks/recommended', 'plugin:@tanstack/eslint-plugin-query/recommended', ], ignorePatterns: ['dist', 'node_modules', '!.*.*'], parser: '@typescript-eslint/parser', + parserOptions: { project: true }, plugins: ['react-refresh', '@typescript-eslint'], rules: { + '@typescript-eslint/array-type': 'off', + '@typescript-eslint/consistent-type-definitions': 'off', + '@typescript-eslint/prefer-nullish-coalescing': 'off', '@typescript-eslint/consistent-type-imports': [ 'warn', { @@ -20,6 +25,12 @@ const config = { }, ], '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }], + '@typescript-eslint/no-misused-promises': [ + 2, + { + checksVoidReturn: { attributes: false }, + }, + ], }, } diff --git a/tsconfig.json b/tsconfig.json index 169b33b0..c6d9363e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -24,6 +24,6 @@ "@/*": ["./src/*"] } }, - "include": ["src"], + "include": ["src", "*.js", "*.ts", "*.cjs", ".prettierrc.js"], "references": [{ "path": "./tsconfig.node.json" }] } From d7502e62d992620b198aff9d655f07d3b3496e9e Mon Sep 17 00:00:00 2001 From: bohdancho Date: Mon, 25 Dec 2023 10:50:00 +0100 Subject: [PATCH 26/27] fix: fix eslint errors with new config --- src/components/DevResetSession.tsx | 4 ++-- src/components/PickUsernameModal.tsx | 2 +- src/components/layout/components/LoginSection.tsx | 2 +- src/features/auth/authTokens.ts | 2 +- src/features/auth/index.ts | 6 +++--- src/features/contests/components/PublishedSession.tsx | 2 +- src/features/contests/routes/index.tsx | 6 +++--- src/features/cube/components/Cube.tsx | 7 ++++++- src/features/dashboard/routes/index.ts | 2 +- src/features/leaderboard/routes/index.tsx | 4 ++-- src/features/reconstructor/api/getReconstruction.ts | 2 +- src/features/solveContest/SolveContest.tsx | 2 +- src/features/solveContest/api/submitSolve.ts | 2 +- 13 files changed, 24 insertions(+), 19 deletions(-) diff --git a/src/components/DevResetSession.tsx b/src/components/DevResetSession.tsx index 7b081ceb..d3b364cc 100644 --- a/src/components/DevResetSession.tsx +++ b/src/components/DevResetSession.tsx @@ -9,8 +9,8 @@ export function DevResetSession() { const resetSession = async () => { try { await axiosClient.delete('/contests/round-session/') - queryClient.refetchQueries({ queryKey: [USER_QUERY_KEY] }) - navigate({ to: '/contest' }) + await queryClient.refetchQueries({ queryKey: [USER_QUERY_KEY] }) + await navigate({ to: '/contest' }) } catch (err) { alert("either you don't have any results to reset or something went wrong") } diff --git a/src/components/PickUsernameModal.tsx b/src/components/PickUsernameModal.tsx index b4ad0a84..fbf67183 100644 --- a/src/components/PickUsernameModal.tsx +++ b/src/components/PickUsernameModal.tsx @@ -22,7 +22,7 @@ export function PickUsernameModal() { } await putChangeUsername(trimmedUsername) - queryClient.invalidateQueries({ queryKey: [USER_QUERY_KEY] }) + await queryClient.invalidateQueries({ queryKey: [USER_QUERY_KEY] }) setIsVisible(false) } diff --git a/src/components/layout/components/LoginSection.tsx b/src/components/layout/components/LoginSection.tsx index 7994c561..8f2348a3 100644 --- a/src/components/layout/components/LoginSection.tsx +++ b/src/components/layout/components/LoginSection.tsx @@ -7,7 +7,7 @@ export function LoginSection() { const { data: userData } = useQuery(userQuery) const handleLogin = useGoogleLogin({ - onSuccess: ({ code }) => login(code), + onSuccess: ({ code }) => void login(code), onError: () => console.log('error'), flow: 'auth-code', }) diff --git a/src/features/auth/authTokens.ts b/src/features/auth/authTokens.ts index 59afa0d1..3d5fc85f 100644 --- a/src/features/auth/authTokens.ts +++ b/src/features/auth/authTokens.ts @@ -4,7 +4,7 @@ const LS_REFRESH_TOKEN = 'refresh-token' const LS_ACCESS_TOKEN = 'access-token' export function createAuthorizedRequestInterceptor(client: AxiosInstance) { - client.interceptors.request.use(async (config) => { + client.interceptors.request.use((config) => { const accessToken = localStorage.getItem(LS_ACCESS_TOKEN) if (accessToken) { config.headers.set('Authorization', `Bearer ${accessToken}`) diff --git a/src/features/auth/index.ts b/src/features/auth/index.ts index 20f5b127..408a0516 100644 --- a/src/features/auth/index.ts +++ b/src/features/auth/index.ts @@ -10,10 +10,10 @@ export async function login(googleCode: string) { const { refresh, access } = response.data setAuthTokens({ refresh, access }) - queryClient.invalidateQueries({ queryKey: [USER_QUERY_KEY] }) + await queryClient.invalidateQueries({ queryKey: [USER_QUERY_KEY] }) } -export function logout() { +export async function logout() { deleteAuthTokens() - queryClient.invalidateQueries({ queryKey: [USER_QUERY_KEY] }) + await queryClient.invalidateQueries({ queryKey: [USER_QUERY_KEY] }) } diff --git a/src/features/contests/components/PublishedSession.tsx b/src/features/contests/components/PublishedSession.tsx index 14a11ddb..7f2596dd 100644 --- a/src/features/contests/components/PublishedSession.tsx +++ b/src/features/contests/components/PublishedSession.tsx @@ -62,7 +62,7 @@ function getBestAndWorstIds(submittedSolves: ContestResultsDTO[number]['solveSet const dnfSolve = submittedSolves.find(({ dnf }) => dnf) const timeArr = submittedSolves .filter(({ timeMs, dnf }) => typeof timeMs === 'number' && !dnf) - .map(({ timeMs }) => timeMs as number) + .map(({ timeMs }) => timeMs!) const bestTime = Math.min(...timeArr) const bestId = submittedSolves.find(({ timeMs }) => timeMs === bestTime)?.id diff --git a/src/features/contests/routes/index.tsx b/src/features/contests/routes/index.tsx index 5aab5829..68382d14 100644 --- a/src/features/contests/routes/index.tsx +++ b/src/features/contests/routes/index.tsx @@ -10,7 +10,7 @@ const allContestsIndexRoute = new Route({ getParentRoute: () => allContestsRoute, path: '/', beforeLoad: async ({ navigate, context: { queryClient } }) => { - navigate({ + void navigate({ to: '$contestNumber', params: { contestNumber: await queryClient.fetchQuery(ongoingContestNumberQuery) }, replace: true, @@ -25,7 +25,7 @@ const contestIndexRoute = new Route({ getParentRoute: () => contestRoute, path: '/', beforeLoad: ({ navigate }) => { - navigate({ to: '$discipline', params: { discipline: DEFAULT_DISCIPLINE }, replace: true }) + void navigate({ to: '$discipline', params: { discipline: DEFAULT_DISCIPLINE }, replace: true }) }, }) @@ -42,7 +42,7 @@ export const contestDisciplineRoute = new Route({ } const query = contestResultsQuery(contestNumber, params.discipline) - queryClient.ensureQueryData(query) + void queryClient.ensureQueryData(query) return query }, component: () => ( diff --git a/src/features/cube/components/Cube.tsx b/src/features/cube/components/Cube.tsx index eff1720c..aadfba90 100644 --- a/src/features/cube/components/Cube.tsx +++ b/src/features/cube/components/Cube.tsx @@ -33,13 +33,18 @@ export function Cube({ scramble, onTimeStart, onSolveFinish, iframeRef }: CubePr ) } +type EventMessage = + | { source: undefined } + | ({ source: typeof POST_MESSAGE_SOURCE } & (TimeStartEvent | SolveFinishEvent)) +type TimeStartEvent = { event: 'timeStart' } +type SolveFinishEvent = { event: 'solveFinish'; payload: { reconstruction: string; timeMs: number } } const POST_MESSAGE_SOURCE = 'vs-solver-integration' const startSolveOnLoad = (() => { let isLoaded = false let savedOnTimeStart: (() => void) | undefined let savedOnSolveFinish: CubeSolveFinishCallback | undefined - window.addEventListener('message', (event) => { + window.addEventListener('message', (event: { data: EventMessage }) => { if (event.data.source !== POST_MESSAGE_SOURCE) { return } diff --git a/src/features/dashboard/routes/index.ts b/src/features/dashboard/routes/index.ts index 892c5bfc..cb20027f 100644 --- a/src/features/dashboard/routes/index.ts +++ b/src/features/dashboard/routes/index.ts @@ -8,7 +8,7 @@ export const dashboardRoute = new Route({ path: '/', component: Dashboard, loader: ({ context: { queryClient } }) => { - queryClient.ensureQueryData(dashboardQuery) + void queryClient.ensureQueryData(dashboardQuery) return dashboardQuery }, }) diff --git a/src/features/leaderboard/routes/index.tsx b/src/features/leaderboard/routes/index.tsx index e022190e..6c4d0ac7 100644 --- a/src/features/leaderboard/routes/index.tsx +++ b/src/features/leaderboard/routes/index.tsx @@ -13,7 +13,7 @@ const indexRoute = new Route({ getParentRoute: () => route, path: '/', beforeLoad: ({ navigate }) => { - navigate({ to: '$discipline', params: { discipline: DEFAULT_DISCIPLINE }, replace: true }) + void navigate({ to: '$discipline', params: { discipline: DEFAULT_DISCIPLINE }, replace: true }) }, }) export const disciplineRoute = new Route({ @@ -26,7 +26,7 @@ export const disciplineRoute = new Route({ } const query = leaderboardQuery(discipline) - queryClient.ensureQueryData(query) + void queryClient.ensureQueryData(query) return query }, component: () => ( diff --git a/src/features/reconstructor/api/getReconstruction.ts b/src/features/reconstructor/api/getReconstruction.ts index 3af49cca..deaeacf8 100644 --- a/src/features/reconstructor/api/getReconstruction.ts +++ b/src/features/reconstructor/api/getReconstruction.ts @@ -19,6 +19,6 @@ async function getReconstruction(solveId: number) { export const reconstructionQuery = (solveId: number | null) => queryOptions({ queryKey: ['reconstructor', solveId], - queryFn: () => getReconstruction(solveId as number), + queryFn: () => getReconstruction(solveId!), enabled: typeof solveId === 'number', }) diff --git a/src/features/solveContest/SolveContest.tsx b/src/features/solveContest/SolveContest.tsx index b98ac580..8f25d61e 100644 --- a/src/features/solveContest/SolveContest.tsx +++ b/src/features/solveContest/SolveContest.tsx @@ -22,7 +22,7 @@ export function SolveContest({ contestNumber, discipline }: SolveContestProps) { } const { currentSolve, submittedSolves } = state - async function solveFinishHandler(result: CubeSolveResult) { + function solveFinishHandler(result: CubeSolveResult) { postSolveResult({ scrambleId: currentSolve.scramble.id, result }) } diff --git a/src/features/solveContest/api/submitSolve.ts b/src/features/solveContest/api/submitSolve.ts index 1e3b4d6d..095f57bd 100644 --- a/src/features/solveContest/api/submitSolve.ts +++ b/src/features/solveContest/api/submitSolve.ts @@ -16,7 +16,7 @@ export const useSubmitSolve = (contestNumber: number, discipline: Discipline) => const isSessionOver = 'detail' in data if (isSessionOver) { - queryClient.invalidateQueries(contestResultsQuery(contestNumber, discipline)) + void queryClient.invalidateQueries(contestResultsQuery(contestNumber, discipline)) return } const newSolvesState = data From 8a988a1c4e01851e97161f24f2ed4c7d71ed3055 Mon Sep 17 00:00:00 2001 From: bohdancho Date: Thu, 28 Dec 2023 19:40:03 +0100 Subject: [PATCH 27/27] infra: update dependencies (vite 5.0) --- bun.lockb | Bin 143053 -> 155825 bytes package.json | 47 +++++++++++++++++++++++------------------------ 2 files changed, 23 insertions(+), 24 deletions(-) diff --git a/bun.lockb b/bun.lockb index 0e26bb31a6b0220e7d23b7b7e087bcf83b8e9120..e92e888599baf709e1eb0dd89af068db8974e602 100755 GIT binary patch delta 47227 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 SSW3Xg4CEHY&x55CHU1w*qXDP@ delta 44867 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 fVW!|T$4w>;I87nJp1wu