diff --git a/client/eslint.config.mjs b/client/eslint.config.mjs index a9839ee14..e358c030e 100644 --- a/client/eslint.config.mjs +++ b/client/eslint.config.mjs @@ -1,30 +1,13 @@ -import { fixupConfigRules, fixupPluginRules } from '@eslint/compat' -import typescriptEslint from '@typescript-eslint/eslint-plugin' -import react from 'eslint-plugin-react' -import globals from 'globals' -import tsParser from '@typescript-eslint/parser' -import path from 'node:path' -import { fileURLToPath } from 'node:url' import js from '@eslint/js' -import { FlatCompat } from '@eslint/eslintrc' - -const __filename = fileURLToPath(import.meta.url) -const __dirname = path.dirname(__filename) -const compat = new FlatCompat({ - baseDirectory: __dirname, - recommendedConfig: js.configs.recommended, - allConfig: js.configs.all, -}) +import tseslint from '@typescript-eslint/eslint-plugin' +import tsParser from '@typescript-eslint/parser' +import eslintReact from '@eslint-react/eslint-plugin' +import prettierConfig from 'eslint-config-prettier' +import prettierPlugin from 'eslint-plugin-prettier' +import globals from 'globals' export default [ - ...fixupConfigRules( - compat.extends( - 'plugin:react/recommended', - 'plugin:@typescript-eslint/recommended', - 'prettier', - 'plugin:prettier/recommended', - ), - ), + js.configs.recommended, { files: [ 'src/**/*.js', @@ -34,44 +17,83 @@ export default [ 'test/**/*.ts', 'test/**/*.tsx', ], - plugins: { - '@typescript-eslint': fixupPluginRules(typescriptEslint), - react: fixupPluginRules(react), + '@typescript-eslint': tseslint, + '@eslint-react': eslintReact, + prettier: prettierPlugin, }, - languageOptions: { - globals: { - ...globals.browser, - ...globals.jest, - }, - parser: tsParser, ecmaVersion: 'latest', sourceType: 'module', - parserOptions: { - ecmaFeatures: { - jsx: true, - }, - + ecmaFeatures: { jsx: true }, project: ['tsconfig.json'], }, + globals: { + ...globals.browser, + ...globals.jest, + }, }, - settings: { - react: { - version: 'detect', + // @eslint-react reads React-version metadata under the `react-x` namespace + // (its underlying rule package is eslint-plugin-react-x), not under `react` + // like the legacy eslint-plugin-react. Pinning explicitly avoids the + // 'detect' codepath that uses APIs removed in ESLint 10. + 'react-x': { + version: '19.2', }, }, - rules: { + ...tseslint.configs.recommended.rules, + ...eslintReact.configs['recommended-typescript'].rules, + ...prettierConfig.rules, + + // Core JS rules that produce false positives in TS — TypeScript already enforces + // these and `no-redeclare` rejects legitimate function overloads. + 'no-undef': 'off', + 'no-redeclare': 'off', 'no-shadow': 'off', - '@typescript-eslint/no-shadow': ['error'], + '@typescript-eslint/no-shadow': 'error', + + // High-value type-safety rules (require type-aware linting via parserOptions.project). + // These catch entire classes of bugs that the type system alone can't detect. + // `await-thenable` is `error` because awaiting a non-Promise is always wrong. + // `no-floating-promises` and `no-misused-promises` are `warn` for now: the legacy + // codebase has many fire-and-forget calls (`doRequest` with callback) that need + // case-by-case review. Promote to `error` once the warning backlog is cleared. + '@typescript-eslint/await-thenable': 'error', + '@typescript-eslint/no-floating-promises': 'warn', + '@typescript-eslint/no-misused-promises': 'warn', + '@typescript-eslint/no-unnecessary-type-assertion': 'warn', + '@typescript-eslint/prefer-nullish-coalescing': 'warn', + '@typescript-eslint/prefer-optional-chain': 'warn', + + // Code-quality rules (no type info needed) + '@typescript-eslint/consistent-type-imports': 'warn', + '@typescript-eslint/no-non-null-assertion': 'warn', + '@typescript-eslint/no-explicit-any': 'warn', + eqeqeq: ['error', 'smart'], + 'no-throw-literal': 'error', + // Allow `!!x` since it is idiomatic and preserves TypeScript narrowing in + // `&&` chains (Boolean(x) does not narrow). Still flags `+x`, `~x.indexOf()`, `'' + x`. + 'no-implicit-coercion': ['warn', { boolean: false }], + 'no-console': ['warn', { allow: ['warn', 'error', 'info'] }], - '@typescript-eslint/no-explicit-any': 'off', + // Intentionally disabled: too noisy or stylistic-only. '@typescript-eslint/strict-boolean-expressions': 'off', '@typescript-eslint/return-await': 'off', + // `set-state-in-effect` is overly aggressive: the project legitimately sets + // state from async data fetches inside useEffect, which is the canonical + // React pattern (see react.dev "You Might Not Need an Effect" — the carve-out + // for fetching/subscriptions still applies). + '@eslint-react/set-state-in-effect': 'off', + // `purity` is a React Compiler hint (flags `new Date()` and `localStorage.getItem()` + // during render). The project doesn't use React Compiler; these reads are + // intentional and safe in our context. + '@eslint-react/purity': 'off', + // `naming-convention-ref-name` is purely stylistic (requires refs to end in `Ref`). + '@eslint-react/naming-convention-ref-name': 'off', 'max-len': [ 'warn', @@ -81,25 +103,12 @@ export default [ ignoreUrls: true, }, ], - - 'react/react-in-jsx-scope': 'off', - - 'react/jsx-filename-extension': [ - 1, - { - extensions: ['.tsx', '.jsx'], - }, - ], - 'prettier/prettier': [ 'error', { endOfLine: 'auto', }, ], - - 'react/prop-types': 'off', - 'react/display-name': 'off', }, }, ] diff --git a/client/package-lock.json b/client/package-lock.json index e1234051d..7652785db 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -43,9 +43,8 @@ "react-router": "7.15.0" }, "devDependencies": { - "@eslint/compat": "2.1.0", - "@eslint/eslintrc": "3.3.5", - "@eslint/js": "9.39.4", + "@eslint-react/eslint-plugin": "5.7.5", + "@eslint/js": "10.0.1", "@playwright/test": "1.59.1", "@testing-library/jest-dom": "6.9.1", "@testing-library/react": "16.3.2", @@ -60,10 +59,9 @@ "copy-webpack-plugin": "14.0.0", "css-loader": "7.1.4", "css-minimizer-webpack-plugin": "8.0.0", - "eslint": "9.39.4", + "eslint": "10.3.0", "eslint-config-prettier": "10.1.8", "eslint-plugin-prettier": "5.5.5", - "eslint-plugin-react": "7.37.5", "fork-ts-checker-webpack-plugin": "9.1.0", "globals": "17.6.0", "html-webpack-plugin": "5.6.7", @@ -493,166 +491,240 @@ "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, - "node_modules/@eslint/compat": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-2.1.0.tgz", - "integrity": "sha512-LgaSCymEpw7tF53xvDw9SNsraPb1IBHxpdABIOM0hW8UAlP8znrjYtuxfR58FSJ3L9BhwD+FaPRFQpZq84Nh6g==", + "node_modules/@eslint-react/ast": { + "version": "5.7.5", + "resolved": "https://registry.npmjs.org/@eslint-react/ast/-/ast-5.7.5.tgz", + "integrity": "sha512-xMrkcIJH/n/YnR9ys0izfeHK0/LEEfC9y+YnM1LdKtGUSKm19vcZ506Nc/X1XfGYkHMaJQJvDnXbdMcGaCI8Sw==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@eslint/core": "^1.2.1" + "@typescript-eslint/types": "^8.59.2", + "@typescript-eslint/typescript-estree": "^8.59.2", + "@typescript-eslint/utils": "^8.59.2", + "string-ts": "^2.3.1" }, "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": ">=22.0.0" }, "peerDependencies": { - "eslint": "^8.40 || 9 || 10" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - } + "eslint": "^10.3.0", + "typescript": "*" } }, - "node_modules/@eslint/config-array": { - "version": "0.21.2", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", - "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "node_modules/@eslint-react/core": { + "version": "5.7.5", + "resolved": "https://registry.npmjs.org/@eslint-react/core/-/core-5.7.5.tgz", + "integrity": "sha512-v4ltzn/Y0gLVpJnFNBTsASPQI3XeIa925kq+bv6qM5i1/8b2MLMNKrf2InDOUVDWrtIRXwuqkeg9WYDVcpqhhA==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@eslint/object-schema": "^2.1.7", - "debug": "^4.3.1", - "minimatch": "^3.1.5" + "@eslint-react/ast": "5.7.5", + "@eslint-react/eslint": "5.7.5", + "@eslint-react/jsx": "5.7.5", + "@eslint-react/shared": "5.7.5", + "@eslint-react/var": "5.7.5", + "@typescript-eslint/scope-manager": "^8.59.2", + "@typescript-eslint/types": "^8.59.2", + "@typescript-eslint/utils": "^8.59.2", + "ts-pattern": "^5.9.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=22.0.0" + }, + "peerDependencies": { + "eslint": "^10.3.0", + "typescript": "*" } }, - "node_modules/@eslint/config-helpers": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", - "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "node_modules/@eslint-react/eslint": { + "version": "5.7.5", + "resolved": "https://registry.npmjs.org/@eslint-react/eslint/-/eslint-5.7.5.tgz", + "integrity": "sha512-PkpHtaVICA0LDl1oDXw7ZF1Nn4ik0TnrG5Urcacr2lwmvwYDvl31hxoBUqNTA+VkYzIDUqKeAUfMXpCtXe3rDw==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@eslint/core": "^0.17.0" + "@typescript-eslint/utils": "^8.59.2" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=22.0.0" + }, + "peerDependencies": { + "eslint": "^10.3.0", + "typescript": "*" } }, - "node_modules/@eslint/config-helpers/node_modules/@eslint/core": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", - "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "node_modules/@eslint-react/eslint-plugin": { + "version": "5.7.5", + "resolved": "https://registry.npmjs.org/@eslint-react/eslint-plugin/-/eslint-plugin-5.7.5.tgz", + "integrity": "sha512-LTNLG1UWhD2W06zu0nqPGv1pRoxLOZuFCOU2ndF8kwYvyF+4VhJoCo4MMTYMJbbmN9ZU61pZFbZAJvPQqXyxkw==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@types/json-schema": "^7.0.15" + "@eslint-react/shared": "5.7.5", + "eslint-plugin-react-dom": "5.7.5", + "eslint-plugin-react-jsx": "5.7.5", + "eslint-plugin-react-naming-convention": "5.7.5", + "eslint-plugin-react-rsc": "5.7.5", + "eslint-plugin-react-web-api": "5.7.5", + "eslint-plugin-react-x": "5.7.5" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=22.0.0" + }, + "peerDependencies": { + "eslint": "^10.3.0", + "typescript": "*" } }, - "node_modules/@eslint/core": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", - "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "node_modules/@eslint-react/jsx": { + "version": "5.7.5", + "resolved": "https://registry.npmjs.org/@eslint-react/jsx/-/jsx-5.7.5.tgz", + "integrity": "sha512-dmtbYB9p1yCneH4Eo16Zv1I7f3/BgGMsiQTm6FD+4KynFouOGZja+7TOU2VbUR4Lf1Hjg3RORA+RyD5VZQpc3Q==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@types/json-schema": "^7.0.15" + "@eslint-react/ast": "5.7.5", + "@eslint-react/eslint": "5.7.5", + "@eslint-react/shared": "5.7.5", + "@eslint-react/var": "5.7.5", + "@typescript-eslint/types": "^8.59.2", + "@typescript-eslint/utils": "^8.59.2", + "ts-pattern": "^5.9.0" }, "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": ">=22.0.0" + }, + "peerDependencies": { + "eslint": "^10.3.0", + "typescript": "*" } }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", - "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "node_modules/@eslint-react/shared": { + "version": "5.7.5", + "resolved": "https://registry.npmjs.org/@eslint-react/shared/-/shared-5.7.5.tgz", + "integrity": "sha512-NV3P16oiQeZBzBVGBMtjM7mxBkEBFcoIb3P898JBzU1S0BdYhR9d/oMTmdjm+81jBmBOBfuzObRZyV4wl6UWAQ==", "dev": true, "license": "MIT", "dependencies": { - "ajv": "^6.14.0", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.1", - "minimatch": "^3.1.5", - "strip-json-comments": "^3.1.1" + "@eslint-react/eslint": "5.7.5", + "@typescript-eslint/utils": "^8.59.2", + "ts-pattern": "^5.9.0", + "zod": "^4.4.3" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=22.0.0" }, - "funding": { - "url": "https://opencollective.com/eslint" + "peerDependencies": { + "eslint": "^10.3.0", + "typescript": "*" } }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "node_modules/@eslint-react/var": { + "version": "5.7.5", + "resolved": "https://registry.npmjs.org/@eslint-react/var/-/var-5.7.5.tgz", + "integrity": "sha512-Zyzb/DGSyXZDBkkRGcTww03JDFgVE3eAqMlRwgqoc28fRzIJNcJBXiUFaiDVGluf0OtZwLCspsvGmI9b0nJn+g==", "dev": true, "license": "MIT", + "dependencies": { + "@eslint-react/ast": "5.7.5", + "@eslint-react/eslint": "5.7.5", + "@typescript-eslint/scope-manager": "^8.59.2", + "@typescript-eslint/types": "^8.59.2", + "@typescript-eslint/utils": "^8.59.2", + "ts-pattern": "^5.9.0" + }, "engines": { - "node": ">=18" + "node": ">=22.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "eslint": "^10.3.0", + "typescript": "*" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.5.tgz", + "integrity": "sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/js": { - "version": "9.39.4", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", - "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", "dev": true, "license": "MIT", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } } }, "node_modules/@eslint/object-schema": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", - "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", "dev": true, "license": "Apache-2.0", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/plugin-kit": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", - "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz", + "integrity": "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.17.0", + "@eslint/core": "^1.2.1", "levn": "^0.4.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", - "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@exodus/bytes": { @@ -2998,6 +3070,13 @@ "@types/estree": "*" } }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", @@ -4073,23 +4152,6 @@ "dequal": "^2.0.3" } }, - "node_modules/array-buffer-byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", - "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "is-array-buffer": "^3.0.5" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", @@ -4097,29 +4159,6 @@ "dev": true, "license": "MIT" }, - "node_modules/array-includes": { - "version": "3.1.9", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", - "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.24.0", - "es-object-atoms": "^1.1.1", - "get-intrinsic": "^1.3.0", - "is-string": "^1.1.1", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/array-union": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", @@ -4143,104 +4182,6 @@ "node": ">=0.10.0" } }, - "node_modules/array.prototype.findlast": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", - "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flat": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", - "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flatmap": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", - "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.tosorted": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", - "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.3", - "es-errors": "^1.3.0", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", - "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "is-array-buffer": "^3.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/asn1js": { "version": "3.0.10", "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.10.tgz", @@ -4285,16 +4226,6 @@ "dev": true, "license": "MIT" }, - "node_modules/async-function": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", - "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, "node_modules/attr-accept": { "version": "2.2.5", "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.5.tgz", @@ -4304,22 +4235,6 @@ "node": ">=4" } }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -4373,6 +4288,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/birecord": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/birecord/-/birecord-0.1.1.tgz", + "integrity": "sha512-VUpsf/qykW0heRlC8LooCq28Kxn3mAqKohhDG/49rrsQ1dT1CXyj/pgXS+5BSRzFTR/3DyIBOqQOrGyZOh71Aw==", + "dev": true, + "license": "(MIT OR Apache-2.0)" + }, "node_modules/body-parser": { "version": "1.20.5", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", @@ -4839,6 +4761,13 @@ "node": ">= 12" } }, + "node_modules/compare-versions": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-6.1.1.tgz", + "integrity": "sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==", + "dev": true, + "license": "MIT" + }, "node_modules/compressible": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", @@ -5409,82 +5338,28 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, - "node_modules/data-view-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", - "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "node_modules/dayjs": { + "version": "1.11.20", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz", + "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" + "ms": "^2.1.3" }, "engines": { - "node": ">= 0.4" + "node": ">=6.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/data-view-byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", - "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/inspect-js" - } - }, - "node_modules/data-view-byte-offset": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", - "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/dayjs": { - "version": "1.11.20", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz", - "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==", - "license": "MIT" - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, "node_modules/decimal.js": { @@ -5572,24 +5447,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/del": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/del/-/del-4.1.1.tgz", @@ -5692,19 +5549,6 @@ "node": ">=6" } }, - "node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/dom-accessibility-api": { "version": "0.5.16", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", @@ -5976,75 +5820,6 @@ "is-arrayish": "^0.2.1" } }, - "node_modules/es-abstract": { - "version": "1.24.2", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", - "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.2", - "arraybuffer.prototype.slice": "^1.0.4", - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "data-view-buffer": "^1.0.2", - "data-view-byte-length": "^1.0.2", - "data-view-byte-offset": "^1.0.1", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "es-set-tostringtag": "^2.1.0", - "es-to-primitive": "^1.3.0", - "function.prototype.name": "^1.1.8", - "get-intrinsic": "^1.3.0", - "get-proto": "^1.0.1", - "get-symbol-description": "^1.1.0", - "globalthis": "^1.0.4", - "gopd": "^1.2.0", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "internal-slot": "^1.1.0", - "is-array-buffer": "^3.0.5", - "is-callable": "^1.2.7", - "is-data-view": "^1.0.2", - "is-negative-zero": "^2.0.3", - "is-regex": "^1.2.1", - "is-set": "^2.0.3", - "is-shared-array-buffer": "^1.0.4", - "is-string": "^1.1.1", - "is-typed-array": "^1.1.15", - "is-weakref": "^1.1.1", - "math-intrinsics": "^1.1.0", - "object-inspect": "^1.13.4", - "object-keys": "^1.1.1", - "object.assign": "^4.1.7", - "own-keys": "^1.0.1", - "regexp.prototype.flags": "^1.5.4", - "safe-array-concat": "^1.1.3", - "safe-push-apply": "^1.0.0", - "safe-regex-test": "^1.1.0", - "set-proto": "^1.0.0", - "stop-iteration-iterator": "^1.1.0", - "string.prototype.trim": "^1.2.10", - "string.prototype.trimend": "^1.0.9", - "string.prototype.trimstart": "^1.0.8", - "typed-array-buffer": "^1.0.3", - "typed-array-byte-length": "^1.0.3", - "typed-array-byte-offset": "^1.0.4", - "typed-array-length": "^1.0.7", - "unbox-primitive": "^1.1.0", - "which-typed-array": "^1.1.19" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -6065,34 +5840,6 @@ "node": ">= 0.4" } }, - "node_modules/es-iterator-helpers": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.3.2.tgz", - "integrity": "sha512-HVLACW1TppGYjJ8H6/jqH/pqOtKRw6wMlrB23xfExmFWxFquAIWCmwoLsOyN96K4a5KbmOf5At9ZUO3GZbetAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.9", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.24.2", - "es-errors": "^1.3.0", - "es-set-tostringtag": "^2.1.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.3.0", - "globalthis": "^1.0.4", - "gopd": "^1.2.0", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.2.0", - "has-symbols": "^1.1.0", - "internal-slot": "^1.1.0", - "iterator.prototype": "^1.1.5", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/es-module-lexer": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", @@ -6113,53 +5860,6 @@ "node": ">= 0.4" } }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-shim-unscopables": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", - "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-to-primitive": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", - "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7", - "is-date-object": "^1.0.5", - "is-symbol": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -6191,33 +5891,30 @@ } }, "node_modules/eslint": { - "version": "9.39.4", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", - "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.3.0.tgz", + "integrity": "sha512-XbEXaRva5cF0ZQB8w6MluHA0kZZfV2DuCMJ3ozyEOHLwDpZX2Lmm/7Pp0xdJmI0GL1W05VH5VwIFHEm1Vcw2gw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.2", - "@eslint/config-helpers": "^0.4.2", - "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.5", - "@eslint/js": "9.39.4", - "@eslint/plugin-kit": "^0.4.1", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.5.5", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", - "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.4.0", - "eslint-visitor-keys": "^4.2.1", - "espree": "^10.4.0", - "esquery": "^1.5.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", @@ -6227,8 +5924,7 @@ "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.5", + "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, @@ -6236,7 +5932,7 @@ "eslint": "bin/eslint.js" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://eslint.org/donate" @@ -6297,61 +5993,168 @@ } } }, - "node_modules/eslint-plugin-react": { - "version": "7.37.5", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", - "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "node_modules/eslint-plugin-react-dom": { + "version": "5.7.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-dom/-/eslint-plugin-react-dom-5.7.5.tgz", + "integrity": "sha512-Offk+olNNmubwu7VjKUKClTIf2/48PyHv0nrLCjNN/3qslql+T93tDbvKwvK9K2p4c0nDZ8vENi5phy06Hy5Tg==", "dev": true, "license": "MIT", "dependencies": { - "array-includes": "^3.1.8", - "array.prototype.findlast": "^1.2.5", - "array.prototype.flatmap": "^1.3.3", - "array.prototype.tosorted": "^1.1.4", - "doctrine": "^2.1.0", - "es-iterator-helpers": "^1.2.1", - "estraverse": "^5.3.0", - "hasown": "^2.0.2", - "jsx-ast-utils": "^2.4.1 || ^3.0.0", - "minimatch": "^3.1.2", - "object.entries": "^1.1.9", - "object.fromentries": "^2.0.8", - "object.values": "^1.2.1", - "prop-types": "^15.8.1", - "resolve": "^2.0.0-next.5", - "semver": "^6.3.1", - "string.prototype.matchall": "^4.0.12", - "string.prototype.repeat": "^1.0.0" + "@eslint-react/ast": "5.7.5", + "@eslint-react/eslint": "5.7.5", + "@eslint-react/jsx": "5.7.5", + "@eslint-react/shared": "5.7.5", + "@typescript-eslint/types": "^8.59.2", + "@typescript-eslint/utils": "^8.59.2", + "compare-versions": "^6.1.1" }, "engines": { - "node": ">=4" + "node": ">=22.0.0" }, "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + "eslint": "^10.3.0", + "typescript": "*" } }, - "node_modules/eslint-plugin-react/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "node_modules/eslint-plugin-react-jsx": { + "version": "5.7.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-jsx/-/eslint-plugin-react-jsx-5.7.5.tgz", + "integrity": "sha512-ACA9EGiYF39ZDt4Huv4zKsBYCBuUO9eKsliMkuKbBnK1QbH3tvpdbwIU4g6vdcOKmS7Y+jf9m3fzezGYtw2mdg==", "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "license": "MIT", + "dependencies": { + "@eslint-react/ast": "5.7.5", + "@eslint-react/core": "5.7.5", + "@eslint-react/eslint": "5.7.5", + "@eslint-react/jsx": "5.7.5", + "@eslint-react/shared": "5.7.5", + "@typescript-eslint/types": "^8.59.2", + "@typescript-eslint/utils": "^8.59.2" + }, + "engines": { + "node": ">=22.0.0" + }, + "peerDependencies": { + "eslint": "^10.3.0", + "typescript": "*" + } + }, + "node_modules/eslint-plugin-react-naming-convention": { + "version": "5.7.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-naming-convention/-/eslint-plugin-react-naming-convention-5.7.5.tgz", + "integrity": "sha512-1M4xXTfXrBqtmkSK5XGFuzEKdMhsk4jtlFIHmxqQ8ZtuByv9VUqm+UBIXqW60HSKER4lmHaJnAEhZCVbPJCaUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-react/ast": "5.7.5", + "@eslint-react/core": "5.7.5", + "@eslint-react/eslint": "5.7.5", + "@eslint-react/var": "5.7.5", + "@typescript-eslint/types": "^8.59.2", + "@typescript-eslint/utils": "^8.59.2", + "ts-pattern": "^5.9.0" + }, + "engines": { + "node": ">=22.0.0" + }, + "peerDependencies": { + "eslint": "^10.3.0", + "typescript": "*" + } + }, + "node_modules/eslint-plugin-react-rsc": { + "version": "5.7.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-rsc/-/eslint-plugin-react-rsc-5.7.5.tgz", + "integrity": "sha512-2uuJ+tPN0QXEyMdGZjIWuWdqLr0/nLViT3GqNYyZo1oL2Fn7EozM7piXxt7PvfsVqhW9xBAHZFX8+2B5Iwiu1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-react/ast": "5.7.5", + "@eslint-react/core": "5.7.5", + "@eslint-react/eslint": "5.7.5", + "@eslint-react/shared": "5.7.5", + "@eslint-react/var": "5.7.5", + "@typescript-eslint/types": "^8.59.2", + "@typescript-eslint/utils": "^8.59.2" + }, + "engines": { + "node": ">=22.0.0" + }, + "peerDependencies": { + "eslint": "^10.3.0", + "typescript": "*" + } + }, + "node_modules/eslint-plugin-react-web-api": { + "version": "5.7.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-web-api/-/eslint-plugin-react-web-api-5.7.5.tgz", + "integrity": "sha512-6lWV7Bydx7ZraMEhcJ/Ra/DKLodBuuP9hlWOQEknUDVXtuyk3l494HqP09Yvh6llBa5Nl7+smXlOP/s8TmKOFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-react/ast": "5.7.5", + "@eslint-react/core": "5.7.5", + "@eslint-react/eslint": "5.7.5", + "@eslint-react/shared": "5.7.5", + "@eslint-react/var": "5.7.5", + "@typescript-eslint/types": "^8.59.2", + "@typescript-eslint/utils": "^8.59.2", + "birecord": "^0.1.1", + "ts-pattern": "^5.9.0" + }, + "engines": { + "node": ">=22.0.0" + }, + "peerDependencies": { + "eslint": "^10.3.0", + "typescript": "*" + } + }, + "node_modules/eslint-plugin-react-x": { + "version": "5.7.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-x/-/eslint-plugin-react-x-5.7.5.tgz", + "integrity": "sha512-oHUoWMWhs2oU3OCrgiVNtzHlIgbZV3GxH/1QPrpCIMT1vn9Nb2TSr8TOLW5nj2kNDglmyCBScBJxo5yxHeS/cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-react/ast": "5.7.5", + "@eslint-react/core": "5.7.5", + "@eslint-react/eslint": "5.7.5", + "@eslint-react/jsx": "5.7.5", + "@eslint-react/shared": "5.7.5", + "@eslint-react/var": "5.7.5", + "@typescript-eslint/scope-manager": "^8.59.2", + "@typescript-eslint/type-utils": "^8.59.2", + "@typescript-eslint/types": "^8.59.2", + "@typescript-eslint/typescript-estree": "^8.59.2", + "@typescript-eslint/utils": "^8.59.2", + "compare-versions": "^6.1.1", + "string-ts": "^2.3.1", + "ts-api-utils": "^2.5.0", + "ts-pattern": "^5.9.0" + }, + "engines": { + "node": ">=22.0.0" + }, + "peerDependencies": { + "eslint": "^10.3.0", + "typescript": "*" } }, "node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" @@ -6370,58 +6173,45 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/@eslint/core": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", - "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", "dev": true, "license": "Apache-2.0", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/espree": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.15.0", + "acorn": "^8.16.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" + "eslint-visitor-keys": "^5.0.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", "dev": true, "license": "Apache-2.0", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" @@ -6855,22 +6645,6 @@ } } }, - "node_modules/for-each": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", - "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/fork-ts-checker-webpack-plugin": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-9.1.0.tgz", @@ -6973,19 +6747,23 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/function.prototype.name": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", - "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "functions-have-names": "^1.2.3", + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", "hasown": "^2.0.2", - "is-callable": "^1.2.7" + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -6994,58 +6772,13 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "dev": true, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/generator-function": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", - "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-nonce": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", - "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", - "license": "MIT", - "engines": { - "node": ">=6" + "engines": { + "node": ">=6" } }, "node_modules/get-proto": { @@ -7062,24 +6795,6 @@ "node": ">= 0.4" } }, - "node_modules/get-symbol-description": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", - "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -7152,23 +6867,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/globalthis": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", - "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.2.1", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/globby": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", @@ -7223,19 +6921,6 @@ "dev": true, "license": "MIT" }, - "node_modules/has-bigints": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", - "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -7259,22 +6944,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-proto": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", - "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -7288,22 +6957,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/hasown": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", @@ -7697,21 +7350,6 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, - "node_modules/internal-slot": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", - "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "hasown": "^2.0.2", - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/interpret": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", @@ -7732,24 +7370,6 @@ "node": ">= 10" } }, - "node_modules/is-array-buffer": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", - "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -7757,42 +7377,6 @@ "dev": true, "license": "MIT" }, - "node_modules/is-async-function": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", - "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "async-function": "^1.0.0", - "call-bound": "^1.0.3", - "get-proto": "^1.0.1", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-bigint": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", - "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-bigints": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -7806,36 +7390,6 @@ "node": ">=8" } }, - "node_modules/is-boolean-object": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", - "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-core-module": { "version": "2.16.2", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", @@ -7852,41 +7406,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-data-view": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", - "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "get-intrinsic": "^1.2.6", - "is-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-date-object": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", - "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-docker": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", @@ -7913,42 +7432,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-finalizationregistry": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", - "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-generator-function": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", - "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.4", - "generator-function": "^2.0.0", - "get-proto": "^1.0.1", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -7997,32 +7480,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-map": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", - "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-negative-zero": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", - "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-network-error": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.2.tgz", @@ -8046,23 +7503,6 @@ "node": ">=0.12.0" } }, - "node_modules/is-number-object": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", - "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-path-cwd": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", @@ -8074,209 +7514,64 @@ } }, "node_modules/is-path-in-cwd": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz", - "integrity": "sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-path-inside": "^2.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/is-path-inside": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-2.1.0.tgz", - "integrity": "sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-is-inside": "^1.0.2" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/is-plain-obj": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", - "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "license": "MIT", - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-potential-custom-element-name": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", - "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-regex": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", - "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-set": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", - "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", - "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-string": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", - "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-symbol": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", - "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-symbols": "^1.1.0", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", - "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz", + "integrity": "sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ==", "dev": true, "license": "MIT", "dependencies": { - "which-typed-array": "^1.1.16" + "is-path-inside": "^2.1.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=6" } }, - "node_modules/is-weakmap": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", - "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "node_modules/is-path-inside": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-2.1.0.tgz", + "integrity": "sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg==", "dev": true, "license": "MIT", - "engines": { - "node": ">= 0.4" + "dependencies": { + "path-is-inside": "^1.0.2" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">=6" } }, - "node_modules/is-weakref": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", - "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "node_modules/is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", "dev": true, "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, "engines": { - "node": ">= 0.4" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-weakset": { + "node_modules/is-plain-object": { "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", - "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" + "isobject": "^3.0.1" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=0.10.0" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-what": { "version": "4.1.16", "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz", @@ -8366,24 +7661,6 @@ "node": ">=8" } }, - "node_modules/iterator.prototype": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", - "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.6", - "get-proto": "^1.0.0", - "has-symbols": "^1.1.0", - "set-function-name": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/jest-regex-util": { "version": "30.4.0", "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.4.0.tgz", @@ -8599,22 +7876,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/jsx-ast-utils": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", - "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-includes": "^3.1.6", - "array.prototype.flat": "^1.3.1", - "object.assign": "^4.1.4", - "object.values": "^1.1.6" - }, - "engines": { - "node": ">=4.0" - } - }, "node_modules/jszip": { "version": "3.10.1", "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", @@ -9108,13 +8369,6 @@ "dev": true, "license": "MIT" }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT" - }, "node_modules/lodash.uniq": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", @@ -9549,35 +8803,6 @@ "license": "MIT", "optional": true }, - "node_modules/node-exports-info": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz", - "integrity": "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "array.prototype.flatmap": "^1.3.3", - "es-errors": "^1.3.0", - "object.entries": "^1.1.9", - "semver": "^6.3.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/node-exports-info/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, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/node-releases": { "version": "2.0.38", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz", @@ -9640,81 +8865,6 @@ "node": ">= 0.4" } }, - "node_modules/object.assign": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", - "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0", - "has-symbols": "^1.1.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.entries": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", - "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.fromentries": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", - "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.values": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", - "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/obuf": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", @@ -9817,24 +8967,6 @@ "integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==", "license": "MIT" }, - "node_modules/own-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", - "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.6", - "object-keys": "^1.1.1", - "safe-push-apply": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -10297,16 +9429,6 @@ "node": ">=18" } }, - "node_modules/possible-typed-array-names": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", - "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, "node_modules/postcss": { "version": "8.5.14", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", @@ -11694,50 +10816,6 @@ "dev": true, "license": "Apache-2.0" }, - "node_modules/reflect.getprototypeof": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", - "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.9", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.7", - "get-proto": "^1.0.1", - "which-builtin-type": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/regexp.prototype.flags": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", - "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-errors": "^1.3.0", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "set-function-name": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/relateurl": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", @@ -11786,30 +10864,6 @@ "dev": true, "license": "MIT" }, - "node_modules/resolve": { - "version": "2.0.0-next.6", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.6.tgz", - "integrity": "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "is-core-module": "^2.16.1", - "node-exports-info": "^1.6.0", - "object-keys": "^1.1.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/resolve-cwd": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", @@ -11903,84 +10957,29 @@ }, "node_modules/rope-sequence": { "version": "1.3.4", - "resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz", - "integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==", - "license": "MIT" - }, - "node_modules/run-applescript": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", - "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/safe-array-concat": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz", - "integrity": "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.9", - "call-bound": "^1.0.4", - "get-intrinsic": "^1.3.0", - "has-symbols": "^1.1.0", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">=0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT" - }, - "node_modules/safe-push-apply": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", - "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz", + "integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==", + "license": "MIT" }, - "node_modules/safe-regex-test": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", - "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", "dev": true, "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-regex": "^1.2.1" - }, "engines": { - "node": ">= 0.4" + "node": ">=18" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -12261,37 +11260,6 @@ "node": ">= 0.4" } }, - "node_modules/set-function-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", - "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-proto": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", - "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", @@ -12592,20 +11560,6 @@ "dev": true, "license": "MIT" }, - "node_modules/stop-iteration-iterator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", - "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "internal-slot": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -12615,103 +11569,12 @@ "safe-buffer": "~5.1.0" } }, - "node_modules/string.prototype.matchall": { - "version": "4.0.12", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", - "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.6", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.6", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "internal-slot": "^1.1.0", - "regexp.prototype.flags": "^1.5.3", - "set-function-name": "^2.0.2", - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.repeat": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", - "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.5" - } - }, - "node_modules/string.prototype.trim": { - "version": "1.2.10", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", - "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "define-data-property": "^1.1.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-object-atoms": "^1.0.0", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimend": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", - "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimstart": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", - "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "node_modules/string-ts": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/string-ts/-/string-ts-2.3.1.tgz", + "integrity": "sha512-xSJq+BS52SaFFAVxuStmx6n5aYZU571uYUnUrPXkPFCfdHyZMMlbP2v2Wx5sNBnAVzq/2+0+mcBLBa3Xa5ubYw==", "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "license": "MIT" }, "node_modules/strip-ansi": { "version": "6.0.1", @@ -12749,19 +11612,6 @@ "node": ">=8" } }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/style-loader": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-4.0.0.tgz", @@ -13443,6 +12293,13 @@ } } }, + "node_modules/ts-pattern": { + "version": "5.9.0", + "resolved": "https://registry.npmjs.org/ts-pattern/-/ts-pattern-5.9.0.tgz", + "integrity": "sha512-6s5V71mX8qBUmlgbrfL33xDUwO0fq48rxAu2LBE11WBeGdpCPOsXksQbZJHvHwhrd3QjUusd3mAOM5Gg0mFBLg==", + "dev": true, + "license": "MIT" + }, "node_modules/tsconfig-paths": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", @@ -13526,84 +12383,6 @@ "node": ">= 0.6" } }, - "node_modules/typed-array-buffer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", - "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/typed-array-byte-length": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", - "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "for-each": "^0.3.3", - "gopd": "^1.2.0", - "has-proto": "^1.2.0", - "is-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-byte-offset": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", - "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "for-each": "^0.3.3", - "gopd": "^1.2.0", - "has-proto": "^1.2.0", - "is-typed-array": "^1.1.15", - "reflect.getprototypeof": "^1.0.9" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-length": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", - "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "is-typed-array": "^1.1.13", - "possible-typed-array-names": "^1.0.0", - "reflect.getprototypeof": "^1.0.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -13648,25 +12427,6 @@ "typescript": ">=4.0.0" } }, - "node_modules/unbox-primitive": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", - "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-bigints": "^1.0.2", - "has-symbols": "^1.1.0", - "which-boxed-primitive": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/undici": { "version": "7.25.0", "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz", @@ -14609,95 +13369,6 @@ "node": ">= 8" } }, - "node_modules/which-boxed-primitive": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", - "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-bigint": "^1.1.0", - "is-boolean-object": "^1.2.1", - "is-number-object": "^1.1.1", - "is-string": "^1.1.1", - "is-symbol": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-builtin-type": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", - "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "function.prototype.name": "^1.1.6", - "has-tostringtag": "^1.0.2", - "is-async-function": "^2.0.0", - "is-date-object": "^1.1.0", - "is-finalizationregistry": "^1.1.0", - "is-generator-function": "^1.0.10", - "is-regex": "^1.2.1", - "is-weakref": "^1.0.2", - "isarray": "^2.0.5", - "which-boxed-primitive": "^1.1.0", - "which-collection": "^1.0.2", - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-collection": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", - "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-map": "^2.0.3", - "is-set": "^2.0.3", - "is-weakmap": "^2.0.2", - "is-weakset": "^2.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-typed-array": { - "version": "1.1.20", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", - "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "for-each": "^0.3.5", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/why-is-node-running": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", @@ -14848,6 +13519,16 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/client/package.json b/client/package.json index 79434685f..90958c4f1 100644 --- a/client/package.json +++ b/client/package.json @@ -90,9 +90,8 @@ "react-router": "7.15.0" }, "devDependencies": { - "@eslint/compat": "2.1.0", - "@eslint/eslintrc": "3.3.5", - "@eslint/js": "9.39.4", + "@eslint-react/eslint-plugin": "5.7.5", + "@eslint/js": "10.0.1", "@playwright/test": "1.59.1", "@testing-library/jest-dom": "6.9.1", "@testing-library/react": "16.3.2", @@ -107,10 +106,9 @@ "copy-webpack-plugin": "14.0.0", "css-loader": "7.1.4", "css-minimizer-webpack-plugin": "8.0.0", - "eslint": "9.39.4", + "eslint": "10.3.0", "eslint-config-prettier": "10.1.8", "eslint-plugin-prettier": "5.5.5", - "eslint-plugin-react": "7.37.5", "fork-ts-checker-webpack-plugin": "9.1.0", "globals": "17.6.0", "html-webpack-plugin": "5.6.7", diff --git a/client/patches/@eslint+eslintrc+3.3.5.patch b/client/patches/@eslint+eslintrc+3.3.5.patch deleted file mode 100644 index 8ec92895b..000000000 --- a/client/patches/@eslint+eslintrc+3.3.5.patch +++ /dev/null @@ -1,105 +0,0 @@ -diff --git a/node_modules/@eslint/eslintrc/dist/eslintrc-universal.cjs b/node_modules/@eslint/eslintrc/dist/eslintrc-universal.cjs -index 6000445..e9d8e80 100644 ---- a/node_modules/@eslint/eslintrc/dist/eslintrc-universal.cjs -+++ b/node_modules/@eslint/eslintrc/dist/eslintrc-universal.cjs -@@ -216,6 +216,7 @@ function emitDeprecationWarning(source, errorCode) { - * Copyright (c) 2015-2017 Evgeny Poberezkin - */ - const metaSchema = { -+ $id: "http://json-schema.org/draft-04/schema", - id: "http://json-schema.org/draft-04/schema#", - $schema: "http://json-schema.org/draft-04/schema#", - description: "Core schema meta-schema", -@@ -374,15 +375,13 @@ var ajvOrig = (additionalOptions = {}) => { - meta: false, - useDefaults: true, - validateSchema: false, -- missingRefs: "ignore", - verbose: true, -- schemaId: "auto", -+ strict: false, -+ defaultMeta: metaSchema.$id, - ...additionalOptions - }); - - ajv.addMetaSchema(metaSchema); -- // eslint-disable-next-line no-underscore-dangle -- part of the API -- ajv._opts.defaultMeta = metaSchema.id; - - return ajv; - }; -diff --git a/node_modules/@eslint/eslintrc/dist/eslintrc.cjs b/node_modules/@eslint/eslintrc/dist/eslintrc.cjs -index 1b76091..4006c05 100644 ---- a/node_modules/@eslint/eslintrc/dist/eslintrc.cjs -+++ b/node_modules/@eslint/eslintrc/dist/eslintrc.cjs -@@ -1449,6 +1449,7 @@ function emitDeprecationWarning(source, errorCode) { - * Copyright (c) 2015-2017 Evgeny Poberezkin - */ - const metaSchema = { -+ $id: "http://json-schema.org/draft-04/schema", - id: "http://json-schema.org/draft-04/schema#", - $schema: "http://json-schema.org/draft-04/schema#", - description: "Core schema meta-schema", -@@ -1607,15 +1608,13 @@ var ajvOrig = (additionalOptions = {}) => { - meta: false, - useDefaults: true, - validateSchema: false, -- missingRefs: "ignore", - verbose: true, -- schemaId: "auto", -+ strict: false, -+ defaultMeta: metaSchema.$id, - ...additionalOptions - }); - - ajv.addMetaSchema(metaSchema); -- // eslint-disable-next-line no-underscore-dangle -- part of the API -- ajv._opts.defaultMeta = metaSchema.id; - - return ajv; - }; -diff --git a/node_modules/@eslint/eslintrc/lib/config-array/override-tester.js b/node_modules/@eslint/eslintrc/lib/config-array/override-tester.js -index 3a445b1..afc8513 100644 ---- a/node_modules/@eslint/eslintrc/lib/config-array/override-tester.js -+++ b/node_modules/@eslint/eslintrc/lib/config-array/override-tester.js -@@ -20,9 +20,7 @@ - import assert from "node:assert"; - import path from "node:path"; - import util from "node:util"; --import minimatch from "minimatch"; -- --const { Minimatch } = minimatch; -+import { minimatch, Minimatch } from "minimatch"; - - const minimatchOpts = { dot: true, matchBase: true }; - -diff --git a/node_modules/@eslint/eslintrc/lib/shared/ajv.js b/node_modules/@eslint/eslintrc/lib/shared/ajv.js -index 7e53d12..82282c2 100644 ---- a/node_modules/@eslint/eslintrc/lib/shared/ajv.js -+++ b/node_modules/@eslint/eslintrc/lib/shared/ajv.js -@@ -19,6 +19,7 @@ import Ajv from "ajv"; - * Copyright (c) 2015-2017 Evgeny Poberezkin - */ - const metaSchema = { -+ $id: "http://json-schema.org/draft-04/schema", - id: "http://json-schema.org/draft-04/schema#", - $schema: "http://json-schema.org/draft-04/schema#", - description: "Core schema meta-schema", -@@ -177,15 +178,13 @@ export default (additionalOptions = {}) => { - meta: false, - useDefaults: true, - validateSchema: false, -- missingRefs: "ignore", - verbose: true, -- schemaId: "auto", -+ strict: false, -+ defaultMeta: metaSchema.$id, - ...additionalOptions - }); - - ajv.addMetaSchema(metaSchema); -- // eslint-disable-next-line no-underscore-dangle -- part of the API -- ajv._opts.defaultMeta = metaSchema.id; - - return ajv; - }; diff --git a/client/patches/eslint+9.39.4.patch b/client/patches/eslint+10.3.0.patch similarity index 100% rename from client/patches/eslint+9.39.4.patch rename to client/patches/eslint+10.3.0.patch diff --git a/client/src/app/layout/AuthenticatedArea/AuthenticatedArea.tsx b/client/src/app/layout/AuthenticatedArea/AuthenticatedArea.tsx index 7475eb143..8d240d7b0 100644 --- a/client/src/app/layout/AuthenticatedArea/AuthenticatedArea.tsx +++ b/client/src/app/layout/AuthenticatedArea/AuthenticatedArea.tsx @@ -1,4 +1,6 @@ -import { PropsWithChildren, Suspense, useEffect } from 'react' +import type { ComponentType, PropsWithChildren } from 'react' +import { Suspense, useEffect } from 'react' +import type { MantineSize } from '@mantine/core' import { ActionIcon, AppShell, @@ -8,7 +10,6 @@ import { Divider, Flex, Group, - MantineSize, Stack, Text, Tooltip, @@ -61,7 +62,7 @@ const AuthenticatedArea = (props: PropsWithChildren) => const links: Array<{ link: string label: string - icon: any + icon: ComponentType<{ size?: number | string; className?: string }> groups: string[] | undefined hideFromGroups?: string[] display?: boolean @@ -137,7 +138,8 @@ const AuthenticatedArea = (props: PropsWithChildren) => minimizeAnimationDuration, ) // only use debounced State if value is false because otherwise the text is formatted weirdly if you expand the navigation - const minimized = opened ? false : minimizedState || !!debouncedMinimized + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- intentionally fall through on `false` so the debounced value is used while collapsing + const minimized = opened ? false : minimizedState || Boolean(debouncedMinimized) const location = useLocation() const navigationType = useNavigationType() @@ -161,6 +163,7 @@ const AuthenticatedArea = (props: PropsWithChildren) => return () => clearInterval(interval) } + // eslint-disable-next-line @eslint-react/exhaustive-deps -- auth object is stable from react-oidc-context; only re-run on auth state or path change }, [auth.isAuthenticated, location.pathname]) useEffect(() => { @@ -169,6 +172,7 @@ const AuthenticatedArea = (props: PropsWithChildren) => } close() + // eslint-disable-next-line @eslint-react/exhaustive-deps -- close is a stable disclosure handler; effect intentionally tracks navigation only }, [location.pathname, navigationType]) return ( @@ -202,7 +206,7 @@ const AuthenticatedArea = (props: PropsWithChildren) => (item) => !item.groups || item.groups.some((role) => auth.user?.groups?.includes(role)), ) - .filter((item) => item.display == undefined || item.display === true) + .filter((item) => item.display === undefined || item.display === true) .filter((item) => item.hideFromGroups ? !item.hideFromGroups.some((role) => auth.user?.groups?.includes(role)) diff --git a/client/src/app/layout/ContentContainer/ContentContainer.tsx b/client/src/app/layout/ContentContainer/ContentContainer.tsx index 9a8cd82b2..7bbf724e7 100644 --- a/client/src/app/layout/ContentContainer/ContentContainer.tsx +++ b/client/src/app/layout/ContentContainer/ContentContainer.tsx @@ -1,5 +1,6 @@ -import { PropsWithChildren } from 'react' -import { Container, MantineSize } from '@mantine/core' +import type { PropsWithChildren } from 'react' +import type { MantineSize } from '@mantine/core' +import { Container } from '@mantine/core' interface IContentContainerProps { size?: number | MantineSize | (string & {}) | undefined diff --git a/client/src/app/layout/PublicArea/PublicArea.tsx b/client/src/app/layout/PublicArea/PublicArea.tsx index 683192932..ce4e03025 100644 --- a/client/src/app/layout/PublicArea/PublicArea.tsx +++ b/client/src/app/layout/PublicArea/PublicArea.tsx @@ -1,6 +1,7 @@ -import { PropsWithChildren } from 'react' +import type { PropsWithChildren } from 'react' import Footer from '../../../components/Footer/Footer' -import { AppShell, Box, Container, Divider, Flex, MantineSize, Stack } from '@mantine/core' +import type { MantineSize } from '@mantine/core' +import { AppShell, Box, Container, Divider, Flex, Stack } from '@mantine/core' import ScrollToTop from '../ScrollToTop/ScrollToTop' import Header from '../../../components/Header/Header' import ContentContainer from '../ContentContainer/ContentContainer' diff --git a/client/src/components/ApplicationData/ApplicationData.tsx b/client/src/components/ApplicationData/ApplicationData.tsx index 01294b857..78f99ba6b 100644 --- a/client/src/components/ApplicationData/ApplicationData.tsx +++ b/client/src/components/ApplicationData/ApplicationData.tsx @@ -1,6 +1,7 @@ -import { IApplication } from '../../requests/responses/application' +import type { IApplication } from '../../requests/responses/application' import { Stack, Group, Grid, Title, Badge, Accordion } from '@mantine/core' -import React, { ReactNode } from 'react' +import type { ReactNode } from 'react' +import React from 'react' import { GLOBAL_CONFIG } from '../../config/global' import { AVAILABLE_COUNTRIES } from '../../config/countries' import { @@ -44,22 +45,22 @@ const ApplicationData = (props: IApplicationDataProps) => { )} - - - + + + @@ -67,7 +68,7 @@ const ApplicationData = (props: IApplicationDataProps) => { @@ -83,14 +84,14 @@ const ApplicationData = (props: IApplicationDataProps) => { @@ -99,7 +100,7 @@ const ApplicationData = (props: IApplicationDataProps) => { @@ -107,7 +108,7 @@ const ApplicationData = (props: IApplicationDataProps) => { diff --git a/client/src/components/ApplicationDeleteButton/ApplicationDeleteButton.tsx b/client/src/components/ApplicationDeleteButton/ApplicationDeleteButton.tsx index 1674dd3f9..cd71fc271 100644 --- a/client/src/components/ApplicationDeleteButton/ApplicationDeleteButton.tsx +++ b/client/src/components/ApplicationDeleteButton/ApplicationDeleteButton.tsx @@ -1,5 +1,6 @@ import { doRequest } from '../../requests/request' -import { ApplicationState, IApplication } from '../../requests/responses/application' +import type { IApplication } from '../../requests/responses/application' +import { ApplicationState } from '../../requests/responses/application' import { showSimpleError, showSimpleSuccess } from '../../utils/notification' import { Button, Modal, Stack, Text, Tooltip, type ButtonProps } from '@mantine/core' import React, { useState } from 'react' @@ -80,7 +81,14 @@ const ApplicationDeleteButton = (props: IApplicationDeleteButtonProps) => { Are you sure you want to permanently delete this application? This action cannot be undone. - diff --git a/client/src/components/ApplicationModal/ApplicationModal.tsx b/client/src/components/ApplicationModal/ApplicationModal.tsx index 21193b5ef..56346bc12 100644 --- a/client/src/components/ApplicationModal/ApplicationModal.tsx +++ b/client/src/components/ApplicationModal/ApplicationModal.tsx @@ -1,4 +1,5 @@ -import { ApplicationState, IApplication } from '../../requests/responses/application' +import type { IApplication } from '../../requests/responses/application' +import { ApplicationState } from '../../requests/responses/application' import { Button, Divider, Modal, Stack } from '@mantine/core' import React from 'react' import ApplicationReviewForm from '../ApplicationReviewForm/ApplicationReviewForm' @@ -23,7 +24,7 @@ const ApplicationModal = (props: IApplicationModalProps) => { } = props return ( - + {application && ( { useEffect(() => { form.reset() + // eslint-disable-next-line @eslint-react/exhaustive-deps -- form is redefined each render; only reset when the modal toggles }, [confirmationModal]) if ( @@ -139,7 +141,9 @@ const ApplicationRejectButton = (props: IApplicationRejectButtonProps) => { {...form.getInputProps('notifyUser', { type: 'checkbox' })} /> ) diff --git a/client/src/components/AuthenticatedFilePreview/AuthenticatedFilePreview.tsx b/client/src/components/AuthenticatedFilePreview/AuthenticatedFilePreview.tsx index 9d4f5b0e4..5756c1ce2 100644 --- a/client/src/components/AuthenticatedFilePreview/AuthenticatedFilePreview.tsx +++ b/client/src/components/AuthenticatedFilePreview/AuthenticatedFilePreview.tsx @@ -1,11 +1,12 @@ -import { ReactNode, useEffect, useState } from 'react' +import type { ReactNode } from 'react' +import { useEffect, useState } from 'react' import { Button, Center, Group, Stack, type BoxProps } from '@mantine/core' import { downloadFile } from '../../utils/blob' import FilePreview from '../FilePreview/FilePreview' import { doRequest } from '../../requests/request' import { showSimpleError } from '../../utils/notification' import { getApiResponseErrorMessage } from '../../requests/handler' -import { UploadFileType } from '../../config/types' +import type { UploadFileType } from '../../config/types' import { getAdjustedFileType } from '../../utils/file' interface IAuthenticatedFilePreviewProps extends BoxProps { diff --git a/client/src/components/AuthenticatedFilePreviewButton/AuthenticatedFilePreviewButton.tsx b/client/src/components/AuthenticatedFilePreviewButton/AuthenticatedFilePreviewButton.tsx index 262025016..b8502726d 100644 --- a/client/src/components/AuthenticatedFilePreviewButton/AuthenticatedFilePreviewButton.tsx +++ b/client/src/components/AuthenticatedFilePreviewButton/AuthenticatedFilePreviewButton.tsx @@ -1,7 +1,8 @@ import { Button, Modal, type ButtonProps } from '@mantine/core' -import { PropsWithChildren, useState } from 'react' +import type { PropsWithChildren } from 'react' +import { useState } from 'react' import { AuthenticatedFilePreview } from '../AuthenticatedFilePreview/AuthenticatedFilePreview' -import { UploadFileType } from '../../config/types' +import type { UploadFileType } from '../../config/types' import { getAdjustedFileType } from '../../utils/file' interface IAuthenticatedFilePreviewButtonProps extends ButtonProps { diff --git a/client/src/components/AvatarUser/AvatarUser.tsx b/client/src/components/AvatarUser/AvatarUser.tsx index 6d5f91841..21ea649f9 100644 --- a/client/src/components/AvatarUser/AvatarUser.tsx +++ b/client/src/components/AvatarUser/AvatarUser.tsx @@ -1,5 +1,6 @@ -import { IMinimalUser } from '../../requests/responses/user' -import { Group, MantineSize, Text } from '@mantine/core' +import type { IMinimalUser } from '../../requests/responses/user' +import type { MantineSize } from '@mantine/core' +import { Group, Text } from '@mantine/core' import { formatUser } from '../../utils/format' import { CustomAvatar } from '../CustomAvatar/CustomAvatar' diff --git a/client/src/components/AvatarUserList/AvatarUserList.tsx b/client/src/components/AvatarUserList/AvatarUserList.tsx index ce1ff3261..7a2a823b7 100644 --- a/client/src/components/AvatarUserList/AvatarUserList.tsx +++ b/client/src/components/AvatarUserList/AvatarUserList.tsx @@ -1,5 +1,6 @@ -import { IMinimalUser } from '../../requests/responses/user' -import { MantineSize, Stack } from '@mantine/core' +import type { IMinimalUser } from '../../requests/responses/user' +import type { MantineSize } from '@mantine/core' +import { Stack } from '@mantine/core' import AvatarUser from '../AvatarUser/AvatarUser' interface IAvatarUserListProps { diff --git a/client/src/components/ConfirmationButton/ConfirmationButton.tsx b/client/src/components/ConfirmationButton/ConfirmationButton.tsx index e5f839cc0..f7471d2ec 100644 --- a/client/src/components/ConfirmationButton/ConfirmationButton.tsx +++ b/client/src/components/ConfirmationButton/ConfirmationButton.tsx @@ -7,63 +7,61 @@ import { Text, type ButtonProps, } from '@mantine/core' -import { forwardRef, ReactNode, useState } from 'react' +import type { ReactNode, Ref } from 'react' +import { useState } from 'react' interface IConfirmationButtonProps extends ButtonProps { confirmationTitle: string confirmationText: string confirmationAdditionalInformation?: ReactNode onClick?: () => unknown + ref?: Ref } const ConfirmationButton = createPolymorphicComponent<'button', IConfirmationButtonProps>( - forwardRef( - ( - { - confirmationTitle, - confirmationText, - confirmationAdditionalInformation: confirmationAdditionalInformation, - children, - onClick, - ...others - }, - ref, - ) => { - const [opened, setOpened] = useState(false) + ({ + confirmationTitle, + confirmationText, + confirmationAdditionalInformation: confirmationAdditionalInformation, + children, + onClick, + ref, + ...others + }: IConfirmationButtonProps) => { + const [opened, setOpened] = useState(false) - return ( - - - - - - {children} - - ) - }, - ), + return ( + + + + + + {children} + + ) + }, ) ConfirmationButton.displayName = 'ConfirmationButton' diff --git a/client/src/components/CustomAvatar/CustomAvatar.tsx b/client/src/components/CustomAvatar/CustomAvatar.tsx index 504690364..ae64772e7 100644 --- a/client/src/components/CustomAvatar/CustomAvatar.tsx +++ b/client/src/components/CustomAvatar/CustomAvatar.tsx @@ -1,6 +1,7 @@ -import { useContext, useEffect, useRef, useState } from 'react' -import { IMinimalUser } from '../../requests/responses/user' -import { Avatar, MantineSize, type BoxProps } from '@mantine/core' +import { use, useEffect, useRef, useState } from 'react' +import type { IMinimalUser } from '../../requests/responses/user' +import type { MantineSize } from '@mantine/core' +import { Avatar, type BoxProps } from '@mantine/core' import { getAvatar, getAvatarPath } from '../../utils/user' import { AuthenticationContext } from '../../providers/AuthenticationContext/context' import { doRequest } from '../../requests/request' @@ -12,7 +13,7 @@ interface ICustomAvatarProps extends BoxProps { export const CustomAvatar = (props: ICustomAvatarProps) => { const { user, size, ...other } = props - const auth = useContext(AuthenticationContext) + const auth = use(AuthenticationContext) const isAuthenticated = auth?.isAuthenticated ?? false const [blobUrl, setBlobUrl] = useState(undefined) const blobUrlRef = useRef(undefined) diff --git a/client/src/components/DocumentEditor/DocumentEditor.tsx b/client/src/components/DocumentEditor/DocumentEditor.tsx index 4f3953d3b..4a7133350 100644 --- a/client/src/components/DocumentEditor/DocumentEditor.tsx +++ b/client/src/components/DocumentEditor/DocumentEditor.tsx @@ -6,7 +6,8 @@ import Underline from '@tiptap/extension-underline' import TextAlign from '@tiptap/extension-text-align' import Superscript from '@tiptap/extension-superscript' import SubScript from '@tiptap/extension-subscript' -import { ChangeEvent, ComponentProps, useEffect, useRef } from 'react' +import type { ChangeEvent, ComponentProps } from 'react' +import { useEffect, useRef } from 'react' import { Input, Text, useMantineColorScheme } from '@mantine/core' import { ensureAbsoluteLinkHref } from '../../utils/format' diff --git a/client/src/components/DropDownMultiSelect/DropDownMultiSelect.tsx b/client/src/components/DropDownMultiSelect/DropDownMultiSelect.tsx index 5f4901bd0..b7b7992aa 100644 --- a/client/src/components/DropDownMultiSelect/DropDownMultiSelect.tsx +++ b/client/src/components/DropDownMultiSelect/DropDownMultiSelect.tsx @@ -47,18 +47,16 @@ const DropDownMultiSelect = ({ const computedColorScheme = useComputedColorScheme() useEffect(() => { - let filteredData = data - if (searchFunction) { - filteredData = searchFunction(search) - } else { - filteredData = data.filter((item) => item.toLowerCase().includes(search.toLowerCase())) - } + const filteredData = searchFunction + ? searchFunction(search) + : data.filter((item) => item.toLowerCase().includes(search.toLowerCase())) setDisplayData( showSelectedOnTop ? filteredData.filter((item) => !selectedItems.includes(item)) : filteredData, ) + // eslint-disable-next-line @eslint-react/exhaustive-deps -- searchFunction is provided inline by callers and showSelectedOnTop is a config prop; intentionally only re-filter when data/search/selection changes }, [data, search, selectedItems]) const content = () => ( diff --git a/client/src/components/EnvironmentBanner/EnvironmentBanner.tsx b/client/src/components/EnvironmentBanner/EnvironmentBanner.tsx index f22da7e7a..e9a6be2cd 100644 --- a/client/src/components/EnvironmentBanner/EnvironmentBanner.tsx +++ b/client/src/components/EnvironmentBanner/EnvironmentBanner.tsx @@ -1,6 +1,7 @@ -import { Group, Text, MantineColor } from '@mantine/core' +import type { MantineColor } from '@mantine/core' +import { Group, Text } from '@mantine/core' import { Warning } from '@phosphor-icons/react' -import { Environment } from '../../config/types' +import type { Environment } from '../../config/types' import { GLOBAL_CONFIG } from '../../config/global' export const ENVIRONMENT_BANNER_HEIGHT = 28 @@ -21,7 +22,7 @@ export const labelByEnvironment: Record = { export const isEnvironmentBannerVisible = (): boolean => { const environment = GLOBAL_CONFIG.environment - return !!environment && environment !== 'production' + return Boolean(environment) && environment !== 'production' } const EnvironmentBanner = () => { diff --git a/client/src/components/FileElement/FileElement.tsx b/client/src/components/FileElement/FileElement.tsx index 920e573f0..f4f891ce3 100644 --- a/client/src/components/FileElement/FileElement.tsx +++ b/client/src/components/FileElement/FileElement.tsx @@ -1,12 +1,5 @@ -import { - Group, - MantineColor, - MantineFontSize, - Paper, - Stack, - Text, - useMantineColorScheme, -} from '@mantine/core' +import type { MantineColor, MantineFontSize } from '@mantine/core' +import { Group, Paper, Stack, Text, useMantineColorScheme } from '@mantine/core' import { FileAudioIcon, FileIcon, FileImageIcon, FilePdfIcon } from '@phosphor-icons/react' import { FileVideoIcon } from '@phosphor-icons/react/dist/ssr' @@ -61,7 +54,7 @@ const FileElement = ({ return ( { } = props const storedRangeKey = rangeStorageKey ? `gantt-chart-range-${rangeStorageKey}` : undefined + const rawStoredRange = storedRangeKey ? localStorage.getItem(storedRangeKey) : null const storedRange = useMemo(() => { - if (!storedRangeKey) { - return undefined - } - - const rawRange = localStorage.getItem(storedRangeKey) - - if (rawRange === null) { + if (!storedRangeKey || rawStoredRange === null) { return undefined } try { - return JSON.parse(rawRange) + return JSON.parse(rawStoredRange) } catch { return undefined } - }, [storedRangeKey && localStorage.getItem(storedRangeKey)]) + }, [storedRangeKey, rawStoredRange]) const [range, setRange] = useState(storedRange) const [collapsedGroups, setCollapsedGroups] = useState([]) @@ -73,12 +71,15 @@ const GanttChart = (props: IGanttChartProps) => { data?.map((row) => ({ groupId: row.groupId, groupNode: row.groupNode, - })) || [], + })) ?? [], (a, b) => a.groupId === b.groupId, ) const currentTime = useMemo(() => Date.now(), []) + const rangeKey = range?.join(',') + const minRangeKey = minRange?.join(',') + const contextValue = useMemo(() => { // Calculate total range based on the provided data const totalRange: DateRange = [ @@ -131,7 +132,7 @@ const GanttChart = (props: IGanttChartProps) => { } return { - data: data || [], + data: data ?? [], currentTime, totalRange, filteredRange, @@ -140,7 +141,8 @@ const GanttChart = (props: IGanttChartProps) => { getTimelineWidth, isVisible, } - }, [data, currentTime, range?.join(','), minRange?.join(','), initialRangeDuration]) + // eslint-disable-next-line @eslint-react/exhaustive-deps -- range/minRange are arrays; joined keys below capture changes deterministically + }, [data, currentTime, rangeKey, minRangeKey, initialRangeDuration]) const { getTimelineLeftPosition, getTimelineWidth, isVisible } = contextValue @@ -153,7 +155,7 @@ const GanttChart = (props: IGanttChartProps) => { } return ( - +
@@ -248,9 +250,9 @@ const GanttChart = (props: IGanttChartProps) => { timelineItem.endDate.getTime(), ]), ) - .map((timelineItem, index) => ( + .map((timelineItem) => (
setHoveredTimelineItem(timelineItem.id)} onMouseLeave={() => setHoveredTimelineItem(undefined)} @@ -305,7 +307,7 @@ const GanttChart = (props: IGanttChartProps) => {
- + ) } diff --git a/client/src/components/GanttChart/components/GanttChartTicks/GanttChartTicks.tsx b/client/src/components/GanttChart/components/GanttChartTicks/GanttChartTicks.tsx index 6fa0737a8..e9a5f4d52 100644 --- a/client/src/components/GanttChart/components/GanttChartTicks/GanttChartTicks.tsx +++ b/client/src/components/GanttChart/components/GanttChartTicks/GanttChartTicks.tsx @@ -7,6 +7,8 @@ const GanttChartTicks = () => { const { filteredRange, currentTime, getTimelineLeftPosition } = useGanttChartContext() + const filteredRangeKey = filteredRange.join(',') + useEffect(() => { if (!ticksRef.current) { return @@ -121,12 +123,14 @@ const GanttChartTicks = () => { } } + const ticksElement = ticksRef.current return () => { - if (ticksRef.current) { - ticksRef.current.innerHTML = '' + if (ticksElement) { + ticksElement.replaceChildren() } } - }, [filteredRange.join(','), currentTime]) + // eslint-disable-next-line @eslint-react/exhaustive-deps -- filteredRange/getTimelineLeftPosition come from context and change together; filteredRangeKey captures the relevant value change + }, [filteredRangeKey, currentTime]) return
} diff --git a/client/src/components/GanttChart/components/GanttChartZoomContainer/GanttChartZoomContainer.tsx b/client/src/components/GanttChart/components/GanttChartZoomContainer/GanttChartZoomContainer.tsx index 12c48a290..105803fec 100644 --- a/client/src/components/GanttChart/components/GanttChartZoomContainer/GanttChartZoomContainer.tsx +++ b/client/src/components/GanttChart/components/GanttChartZoomContainer/GanttChartZoomContainer.tsx @@ -1,6 +1,8 @@ -import { TouchEvent, Touch, WheelEvent, useEffect, useRef, PropsWithChildren } from 'react' +import type { TouchEvent, Touch, WheelEvent, PropsWithChildren } from 'react' +import { useEffect, useRef } from 'react' import * as classes from '../../GanttChart.module.css' -import { DateRange, useGanttChartContext } from '../../context' +import type { DateRange } from '../../context' +import { useGanttChartContext } from '../../context' const GanttChartZoomContainer = (props: PropsWithChildren) => { const { children } = props diff --git a/client/src/components/GanttChart/context.ts b/client/src/components/GanttChart/context.ts index 3cf7f7518..e6f1ae443 100644 --- a/client/src/components/GanttChart/context.ts +++ b/client/src/components/GanttChart/context.ts @@ -1,4 +1,5 @@ -import React, { Dispatch, ReactNode, SetStateAction, useContext } from 'react' +import type { Dispatch, ReactNode, SetStateAction } from 'react' +import React, { use } from 'react' export type DateRange = [number, number] @@ -34,7 +35,7 @@ export interface IGanttChartContext { export const GanttChartContext = React.createContext(undefined) export function useGanttChartContext() { - const data = useContext(GanttChartContext) + const data = use(GanttChartContext) if (!data) { throw new Error('GanttChartContext not initialized') diff --git a/client/src/components/InterviewSlotInformation/InterviewSlotInformation.tsx b/client/src/components/InterviewSlotInformation/InterviewSlotInformation.tsx index 60f1ea7f5..1d983baa1 100644 --- a/client/src/components/InterviewSlotInformation/InterviewSlotInformation.tsx +++ b/client/src/components/InterviewSlotInformation/InterviewSlotInformation.tsx @@ -1,5 +1,5 @@ import { Stack } from '@mantine/core' -import { IInterviewSlot } from '../../requests/responses/interview' +import type { IInterviewSlot } from '../../requests/responses/interview' import InterviewInfoItem from '../InterviewInfoItem/InterviewInfoItem' import { CalendarBlankIcon, ClockIcon, MapPinIcon, WebcamIcon } from '@phosphor-icons/react' diff --git a/client/src/components/KeycloakUserAutocomplete.tsx/KeycloakUserAutocomplete.tsx b/client/src/components/KeycloakUserAutocomplete.tsx/KeycloakUserAutocomplete.tsx index b5b3cd521..91f6f2ae3 100644 --- a/client/src/components/KeycloakUserAutocomplete.tsx/KeycloakUserAutocomplete.tsx +++ b/client/src/components/KeycloakUserAutocomplete.tsx/KeycloakUserAutocomplete.tsx @@ -5,8 +5,8 @@ import { showSimpleError } from '../../utils/notification' import { doRequest } from '../../requests/request' import { getApiResponseErrorMessage } from '../../requests/handler' import UserInformationRow from '../UserInformationRow/UserInformationRow' -import { ILightUser } from '../../requests/responses/user' -import { IKeycloakUserElement } from '../../requests/responses/keycloakUser' +import type { ILightUser } from '../../requests/responses/user' +import type { IKeycloakUserElement } from '../../requests/responses/keycloakUser' interface KeycloakUserAutocompleteProps { selectedLabel: string @@ -29,7 +29,7 @@ const KeycloakUserAutocomplete = ({ const [debouncedSearchKey] = useDebouncedValue(searchKey, 300) const [userOptions, setUserOptions] = useState([]) const [loadingUsers, setLoadingUsers] = useState(false) - const [selectedUsername, setSelectedUsername] = useState(previousUser?.universityId || '') + const [selectedUsername, setSelectedUsername] = useState(previousUser?.universityId ?? '') const getUserOptionValue = (user: { firstName: string diff --git a/client/src/components/LabeledItem/LabeledItem.tsx b/client/src/components/LabeledItem/LabeledItem.tsx index 724ece6c2..9fa849d56 100644 --- a/client/src/components/LabeledItem/LabeledItem.tsx +++ b/client/src/components/LabeledItem/LabeledItem.tsx @@ -1,4 +1,5 @@ -import React, { ReactNode } from 'react' +import type { ReactNode } from 'react' +import React from 'react' import { ActionIcon, CopyButton, Group, Input, Text, Tooltip } from '@mantine/core' import { Check, Copy } from '@phosphor-icons/react' diff --git a/client/src/components/Logo/Logo.tsx b/client/src/components/Logo/Logo.tsx index 7ec61a124..f4fdd5f7c 100644 --- a/client/src/components/Logo/Logo.tsx +++ b/client/src/components/Logo/Logo.tsx @@ -5,6 +5,16 @@ interface ILogoProps { color?: string } +// SVG path data is one continuous attribute value that cannot be wrapped. + +const PATH_OUTLINE = + // eslint-disable-next-line max-len -- SVG path data is one unwrappable attribute value + 'M37.29,24.04c-2.03-3.65-4.09-8.62-5.52-12.56l-2.46-6.82c-.06-.17-.26-.36-.39-.4-.55-.17-1.2.66-1.84.92l-1.24-3.51c-.4-1.13-1.39-1.38-2.42-1.01L5.47,7.06c-2.04.73-3.48,2.92-2.62,5.12l5.08,13.06,4.1,9.62c.16.38.36.71.57,1.04.55.87,1.57,1.23,2.54.95l1.44-.42.22,2.6c.02.18.21.41.36.45.19.04.49-.01.63-.13l1.23-1.05,1.12.65c.13.08.45.09.61.05.14-.04.39-.25.37-.43l-.41-3.08,14.83-3.5c.43-.1.62-.48.59-.87l-.1-1.45c-.05-.72.8-.36,1.01-.97.08-.24.02-.57-.11-.79l-1.16-1.98,1.33-.87c.34-.23.37-.68.18-1.01ZM13.21,34.01l-4.36-10.25-4.69-12.11c-.4-1.04.05-2.1.95-2.72,1.68,4.75,3.42,9.41,5.36,14.06,1.16,2.72,2.3,5.36,3.63,8.05-.64.87-.81,1.78-.89,2.96ZM11.2,21.06c-1.74-4.24-3.38-8.45-4.83-12.82L23.98,2.01c.33-.12.54.05.65.36l4.1,11.49c1.02,2.86,2.15,5.59,3.45,8.4-1.73,2.04-5.57,4.08-8.04,4.96l-8.79,3.13c-1.53-3.09-2.86-6.14-4.14-9.28ZM15.07,33.36l1.09-.28.04.46-1.31.37c-.09-.16,0-.45.17-.56ZM14.93,35.5c-.13-.15-.16-.56-.03-.73l1.44-.44.1.77-1.51.4ZM18.81,36.85c-.2.04-.4.27-.6.51l-.41-3.6c.38-.13.73-.21,1.09-.26l.61,3.61c-.23-.14-.47-.31-.68-.26ZM20.48,34.19l-.17-.75,14.28-3.78c.08.36.1.67.1,1.11l-14.21,3.41ZM28.67,29.98l-8.52,2.4c-.12.03-.24-.26-.2-.35.03-.09.16-.2.29-.24l8.28-2.75c2.1-.7,4.03-1.56,6.03-2.57l.91,1.57-6.79,1.95ZM31.3,26.53c-2.85,1.23-5.7,2.26-8.64,3.29l-7.39,2.5c.09-.54.56-.72,1.02-.89l8.73-3.11c2.94-1.05,6.42-3.08,8.47-5.45.24-.28.28-.61.12-.93-1.03-2.14-1.97-4.25-2.79-6.49l-3.36-9.15c.28-.17.55-.29.87-.38l3.72,10.14,1.11,2.61c.8,1.9,1.67,3.7,2.67,5.56-1.47.91-2.96,1.62-4.53,2.3Z' + +const PATH_MAJOR_DETAIL = + // eslint-disable-next-line max-len -- SVG path data is one unwrappable attribute value + 'M18.59,17.84c.47,1.32-.89,1.53-.69,1.92.04.08.27.16.39.11l3.98-1.5c.16-.06.29-.29.26-.42-.13-.53-1.44.38-2.03-1.23l-2.58-7.07,1.89-.69c1.9-.7,2.55.74,3.06.32l-.87-2.3c-.04-.11-.39-.08-.52-.04l-10.31,3.75c-.11.04-.21.31-.17.42l.59,1.81c.03.1.22.27.31.24.49-.16.06-1.38,1.69-1.96l2.31-.83,2.69,7.46Z' + const Logo = ({ size = 44, color }: ILogoProps) => { const theme = useMantineTheme() const computedColorScheme = useComputedColorScheme() @@ -23,14 +33,8 @@ const Logo = ({ size = 44, color }: ILogoProps) => { strokeWidth='0.1' fill={strokeColor} > - - + + presentation.location || 'Not available', + render: (presentation) => presentation.location ?? 'Not available', }, streamUrl: { accessor: 'streamUrl', @@ -227,7 +228,7 @@ const PresentationsTable = row.thesisId === editPresentationModal?.thesisId)} presentation={editPresentationModal} - opened={!!editPresentationModal} + opened={Boolean(editPresentationModal)} onClose={() => setEditPresentationModal(undefined)} onChange={onChange} /> diff --git a/client/src/components/PresentationsTable/components/ReplacePresentationModal/ReplacePresentationModal.tsx b/client/src/components/PresentationsTable/components/ReplacePresentationModal/ReplacePresentationModal.tsx index 29b10f85a..f24362116 100644 --- a/client/src/components/PresentationsTable/components/ReplacePresentationModal/ReplacePresentationModal.tsx +++ b/client/src/components/PresentationsTable/components/ReplacePresentationModal/ReplacePresentationModal.tsx @@ -4,7 +4,7 @@ import { useEffect } from 'react' import { useThesisUpdateAction } from '../../../../providers/ThesisProvider/hooks' import { Accordion, Alert, Button, Modal, Select, Stack, TextInput } from '@mantine/core' import { doRequest } from '../../../../requests/request' -import { +import type { IPublishedPresentation, IPublishedThesis, IThesis, @@ -71,6 +71,7 @@ const ReplacePresentationModal = (props: IReplacePresentationModalProps) => { useEffect(() => { form.validateField('location') form.validateField('streamUrl') + // eslint-disable-next-line @eslint-react/exhaustive-deps -- form is redefined each render; intentionally re-validate only on value changes }, [form.values.streamUrl, form.values.location]) useEffect(() => { @@ -78,8 +79,8 @@ const ReplacePresentationModal = (props: IReplacePresentationModalProps) => { form.setInitialValues({ type: presentation.type, visibility: presentation.visibility, - location: presentation.location || '', - streamUrl: presentation.streamUrl || '', + location: presentation.location ?? '', + streamUrl: presentation.streamUrl ?? '', language: presentation.language, date: new Date(presentation.scheduledAt), }) @@ -95,40 +96,48 @@ const ReplacePresentationModal = (props: IReplacePresentationModalProps) => { } form.reset() + // eslint-disable-next-line @eslint-react/exhaustive-deps -- form is stable across renders; resetting on form identity change would loop }, [opened, presentation]) const [replacing, onReplacePresentation] = useThesisUpdateAction( async () => { - const response = await doRequest( - presentation - ? `/v2/theses/${presentation.thesisId}/presentations/${presentation.presentationId}` - : `/v2/theses/${thesis!.thesisId}/presentations`, - { - method: presentation ? 'PUT' : 'POST', - requiresAuth: true, - data: { - type: form.values.type, - visibility: form.values.visibility, - location: form.values.location, - streamUrl: form.values.streamUrl, - language: form.values.language, - date: form.values.date, - }, + if (!presentation && !thesis) { + throw new Error('Either a thesis or an existing presentation is required') + } + + const url = presentation + ? `/v2/theses/${presentation.thesisId}/presentations/${presentation.presentationId}` + : `/v2/theses/${thesis?.thesisId ?? ''}/presentations` + + const response = await doRequest(url, { + method: presentation ? 'PUT' : 'POST', + requiresAuth: true, + data: { + type: form.values.type, + visibility: form.values.visibility, + location: form.values.location, + streamUrl: form.values.streamUrl, + language: form.values.language, + date: form.values.date, }, - ) + }) if (response.ok) { onClose() // Find the updated or newly created presentation + const targetScheduledAt = + form.values.date instanceof Date + ? form.values.date.toISOString() + : form.values.date + ? new Date(form.values.date).toISOString() + : undefined const updatedPresentation = response.data.presentations?.find((p) => presentation ? p.presentationId === presentation.presentationId - : // For new presentation, pick the one with the latest scheduledAt - p.scheduledAt === - (form.values.date instanceof Date - ? form.values.date.toISOString() - : new Date(form.values.date as any).toISOString()), + : // For new presentation, match by the freshly-submitted scheduledAt. + // Guard against picking a record with scheduledAt === null when the form date is missing. + targetScheduledAt !== undefined && p.scheduledAt === targetScheduledAt, ) onChange?.(updatedPresentation) diff --git a/client/src/components/PresentationsTable/components/SchedulePresentationModal/SchedulePresentationModal.tsx b/client/src/components/PresentationsTable/components/SchedulePresentationModal/SchedulePresentationModal.tsx index c16a9179a..81a052636 100644 --- a/client/src/components/PresentationsTable/components/SchedulePresentationModal/SchedulePresentationModal.tsx +++ b/client/src/components/PresentationsTable/components/SchedulePresentationModal/SchedulePresentationModal.tsx @@ -1,4 +1,4 @@ -import { +import type { IPublishedPresentation, IThesis, IThesisPresentation, @@ -65,7 +65,12 @@ const SchedulePresentationModal = (props: ISchedulePresentationModalProps) => { }, [presentation?.presentationId]) return ( - onClose()} centered> + onClose()} + centered + > { diff --git a/client/src/components/UserCard/UserCard.tsx b/client/src/components/UserCard/UserCard.tsx index c4a6240ce..fb981d15e 100644 --- a/client/src/components/UserCard/UserCard.tsx +++ b/client/src/components/UserCard/UserCard.tsx @@ -1,5 +1,5 @@ import { Group, Paper, Stack, Title, Text } from '@mantine/core' -import { ILightUser } from '../../requests/responses/user' +import type { ILightUser } from '../../requests/responses/user' import { CustomAvatar } from '../CustomAvatar/CustomAvatar' import { formateStudyProgram, formatThesisType } from '../../utils/format' diff --git a/client/src/components/UserInformationForm/UserInformationForm.tsx b/client/src/components/UserInformationForm/UserInformationForm.tsx index 959ed7826..d52c8182f 100644 --- a/client/src/components/UserInformationForm/UserInformationForm.tsx +++ b/client/src/components/UserInformationForm/UserInformationForm.tsx @@ -1,5 +1,5 @@ import { isEmail, isNotEmpty, useForm } from '@mantine/form' -import { IUpdateUserInformationPayload } from '../../requests/payloads/user' +import type { IUpdateUserInformationPayload } from '../../requests/payloads/user' import { Anchor, Button, @@ -159,23 +159,24 @@ const UserInformationForm = (props: IUserInformationFormProps) => { useEffect(() => { form.setValues({ - email: user?.email || '', - matriculationNumber: user?.matriculationNumber || '', - firstName: user?.firstName || '', - lastName: user?.lastName || '', - gender: user?.gender || '', - nationality: user?.nationality || '', - studyDegree: user?.studyDegree || '', - studyProgram: user?.studyProgram || '', + email: user?.email ?? '', + matriculationNumber: user?.matriculationNumber ?? '', + firstName: user?.firstName ?? '', + lastName: user?.lastName ?? '', + gender: user?.gender ?? '', + nationality: user?.nationality ?? '', + studyDegree: user?.studyDegree ?? '', + studyProgram: user?.studyProgram ?? '', semester: user?.enrolledAt ? enrollmentDateToSemester(user.enrolledAt).toString() : '', - researchGroupName: user?.researchGroupName || '', - specialSkills: user?.specialSkills || '', - interests: user?.interests || '', - projects: user?.projects || '', + researchGroupName: user?.researchGroupName ?? '', + specialSkills: user?.specialSkills ?? '', + interests: user?.interests ?? '', + projects: user?.projects ?? '', customData: Object.fromEntries( - Object.keys(GLOBAL_CONFIG.custom_data).map((key) => [key, user.customData?.[key] || '']), + Object.keys(GLOBAL_CONFIG.custom_data).map((key) => [key, user.customData?.[key] ?? '']), ), }) + // eslint-disable-next-line @eslint-react/exhaustive-deps -- form is stable; including it would loop on every form value change }, [user]) useApiPdfFile( diff --git a/client/src/components/UserInformationForm/components/AvatarInput/AvatarInput.tsx b/client/src/components/UserInformationForm/components/AvatarInput/AvatarInput.tsx index feb0f261a..4dadd915e 100644 --- a/client/src/components/UserInformationForm/components/AvatarInput/AvatarInput.tsx +++ b/client/src/components/UserInformationForm/components/AvatarInput/AvatarInput.tsx @@ -17,7 +17,7 @@ import { Dropzone, IMAGE_MIME_TYPE } from '@mantine/dropzone' import { useMemo, useRef, useState } from 'react' import { doRequest } from '../../../../requests/request' import { showSimpleError } from '../../../../utils/notification' -import { IUser } from '../../../../requests/responses/user' +import type { IUser } from '../../../../requests/responses/user' const IMPORT_TOOLTIP = 'Imports your profile picture from Gravatar (gravatar.com), a US-based service.' + @@ -111,7 +111,9 @@ const AvatarInput = (props: IAvatarInputProps) => { diff --git a/client/src/components/UserInformationRow/UserInformationRow.tsx b/client/src/components/UserInformationRow/UserInformationRow.tsx index e7c5bfe23..4fc8c88b1 100644 --- a/client/src/components/UserInformationRow/UserInformationRow.tsx +++ b/client/src/components/UserInformationRow/UserInformationRow.tsx @@ -1,5 +1,5 @@ import { Badge, Flex, Group, Stack, Text } from '@mantine/core' -import { ILightUser } from '../../requests/responses/user' +import type { ILightUser } from '../../requests/responses/user' import { CustomAvatar } from '../CustomAvatar/CustomAvatar' type IUserInformationRowProps = { diff --git a/client/src/components/UserMultiSelect/UserMultiSelect.tsx b/client/src/components/UserMultiSelect/UserMultiSelect.tsx index 666c79c4e..6982530b1 100644 --- a/client/src/components/UserMultiSelect/UserMultiSelect.tsx +++ b/client/src/components/UserMultiSelect/UserMultiSelect.tsx @@ -1,8 +1,8 @@ import { MultiSelect } from '@mantine/core' import { useEffect, useState } from 'react' import { doRequest } from '../../requests/request' -import { PaginationResponse } from '../../requests/responses/pagination' -import { ILightUser } from '../../requests/responses/user' +import type { PaginationResponse } from '../../requests/responses/pagination' +import type { ILightUser } from '../../requests/responses/user' import { useDebouncedValue } from '@mantine/hooks' import type { GetInputPropsReturnType } from '@mantine/form' import { formatUser } from '../../utils/format' @@ -56,6 +56,8 @@ export const UserMultiSelect = (props: IUserMultiSelectProps) => { const [debouncedSearchValue] = useDebouncedValue(searchValue, 500) + const groupsKey = groups.join(',') + useEffect(() => { // Skip fetching when disabled or until the user interacts with the dropdown if (disabled || fetchVersion === 0) { @@ -71,7 +73,7 @@ export const UserMultiSelect = (props: IUserMultiSelectProps) => { method: 'GET', requiresAuth: true, params: { - groups: groups.join(','), + groups: groupsKey, searchQuery: debouncedSearchValue, page: '0', limit: '100', @@ -100,7 +102,8 @@ export const UserMultiSelect = (props: IUserMultiSelectProps) => { } }, ) - }, [groups.join(','), debouncedSearchValue, fetchVersion, disabled]) + // eslint-disable-next-line @eslint-react/exhaustive-deps -- `selected` is read inside setData updater to preserve picks; refetching when selection changes would loop + }, [groupsKey, debouncedSearchValue, fetchVersion, disabled]) // Combine fetched data with initialUsers so selected options are always visible, // even before the first API call diff --git a/client/src/config/global.ts b/client/src/config/global.ts index 12ceb7915..37b3bc9f6 100644 --- a/client/src/config/global.ts +++ b/client/src/config/global.ts @@ -1,4 +1,4 @@ -import { Environment, IGlobalConfig } from './types' +import type { Environment, IGlobalConfig } from './types' const parseEnvironment = (value: string | undefined): Environment | undefined => { if (!value) { @@ -17,7 +17,7 @@ const parseEnvironment = (value: string | undefined): Environment | undefined => } const getEnvironmentVariable = (key: string, useJson = false): T | undefined => { - const value = process.env[key] || window.RUNTIME_ENVIRONMENT_VARIABLES?.[key] + const value = process.env[key] ?? window.RUNTIME_ENVIRONMENT_VARIABLES?.[key] if (!value) { return undefined @@ -31,48 +31,48 @@ const getEnvironmentVariable = (key: string, useJson = false): T | u } export const GLOBAL_CONFIG: IGlobalConfig = { - title: getEnvironmentVariable('APPLICATION_TITLE') || 'ThesisManagement', + title: getEnvironmentVariable('APPLICATION_TITLE') ?? 'ThesisManagement', - chair_name: getEnvironmentVariable('CHAIR_NAME') || 'ThesisManagement', - chair_url: getEnvironmentVariable('CHAIR_URL') || window.origin, + chair_name: getEnvironmentVariable('CHAIR_NAME') ?? 'ThesisManagement', + chair_url: getEnvironmentVariable('CHAIR_URL') ?? window.origin, environment: parseEnvironment(getEnvironmentVariable('ENVIRONMENT')), - allow_suggested_topics: (getEnvironmentVariable('ALLOW_SUGGESTED_TOPICS') || 'true') === 'true', + allow_suggested_topics: (getEnvironmentVariable('ALLOW_SUGGESTED_TOPICS') ?? 'true') === 'true', - genders: getEnvironmentVariable>('GENDERS', true) || { + genders: getEnvironmentVariable>('GENDERS', true) ?? { MALE: 'Male', FEMALE: 'Female', OTHER: 'Other', PREFER_NOT_TO_SAY: 'Prefer not to say', }, - study_degrees: getEnvironmentVariable>('STUDY_DEGREES', true) || { + study_degrees: getEnvironmentVariable>('STUDY_DEGREES', true) ?? { BACHELOR: 'Bachelor', MASTER: 'Master', }, - study_programs: getEnvironmentVariable>('STUDY_PROGRAMS', true) || { + study_programs: getEnvironmentVariable>('STUDY_PROGRAMS', true) ?? { COMPUTER_SCIENCE: 'Computer Science', INFORMATION_SYSTEMS: 'Information Systems', GAMES_ENGINEERING: 'Games Engineering', MANAGEMENT_AND_TECHNOLOGY: 'Management and Technology', OTHER: 'Other', }, - topic_views_options: getEnvironmentVariable('TOPIC_VIEWS_OPTIONS', true) || { + topic_views_options: getEnvironmentVariable('TOPIC_VIEWS_OPTIONS', true) ?? { OPEN: 'Open Topics', PUBLISHED: 'Published Topics', }, research_groups_location: getEnvironmentVariable>( 'RESEARCH_GROUPS_LOCATION', true, - ) || { + ) ?? { GARCHING: 'Garching', MUNICH: 'Munich', HEILBRONN: 'Heilbronn', WEIHENSTEPHAN: 'Weihenstephan', }, - thesis_types: getEnvironmentVariable('THESIS_TYPES', true) || { + thesis_types: getEnvironmentVariable('THESIS_TYPES', true) ?? { BACHELOR: { long: 'Bachelor Thesis', short: 'BA', @@ -91,19 +91,19 @@ export const GLOBAL_CONFIG: IGlobalConfig = { }, }, - languages: getEnvironmentVariable>('LANGUAGES', true) || { + languages: getEnvironmentVariable>('LANGUAGES', true) ?? { ENGLISH: 'English', GERMAN: 'German', }, - custom_data: getEnvironmentVariable('CUSTOM_DATA', true) || { + custom_data: getEnvironmentVariable('CUSTOM_DATA', true) ?? { GITHUB: { label: 'Github Username', required: false, }, }, - thesis_files: getEnvironmentVariable('THESIS_FILES', true) || { + thesis_files: getEnvironmentVariable('THESIS_FILES', true) ?? { PRESENTATION: { label: 'Presentation', description: 'Presentation (PDF)', @@ -124,14 +124,14 @@ export const GLOBAL_CONFIG: IGlobalConfig = { }, }, - server_host: (getEnvironmentVariable('SERVER_HOST') || 'http://localhost:8180').replace( + server_host: (getEnvironmentVariable('SERVER_HOST') ?? 'http://localhost:8180').replace( /\/+$/, '', ), keycloak: { - host: getEnvironmentVariable('KEYCLOAK_HOST') || 'http://localhost:8181', - realm: getEnvironmentVariable('KEYCLOAK_REALM_NAME') || 'thesis-management', - client_id: getEnvironmentVariable('KEYCLOAK_CLIENT_ID') || 'thesis-management-app', + host: getEnvironmentVariable('KEYCLOAK_HOST') ?? 'http://localhost:8181', + realm: getEnvironmentVariable('KEYCLOAK_REALM_NAME') ?? 'thesis-management', + client_id: getEnvironmentVariable('KEYCLOAK_CLIENT_ID') ?? 'thesis-management-app', }, } diff --git a/client/src/hooks/authentication.ts b/client/src/hooks/authentication.ts index d977cf0b0..492608fdc 100644 --- a/client/src/hooks/authentication.ts +++ b/client/src/hooks/authentication.ts @@ -1,9 +1,9 @@ -import { useContext } from 'react' +import { use } from 'react' import { AuthenticationContext } from '../providers/AuthenticationContext/context' import { useLocalStorage } from './local-storage' export function useAuthenticationContext() { - const context = useContext(AuthenticationContext) + const context = use(AuthenticationContext) if (!context) { throw new Error('Authentication context not initialized') diff --git a/client/src/hooks/fetcher.ts b/client/src/hooks/fetcher.ts index b20068d16..867715b06 100644 --- a/client/src/hooks/fetcher.ts +++ b/client/src/hooks/fetcher.ts @@ -1,10 +1,10 @@ -import { IThesis } from '../requests/responses/thesis' +import type { IThesis } from '../requests/responses/thesis' import { useEffect, useState } from 'react' import { doRequest } from '../requests/request' import { showSimpleError } from '../utils/notification' import { getApiResponseErrorMessage } from '../requests/handler' -import { ITopic, ITopicOverview } from '../requests/responses/topic' -import { PaginationResponse } from '../requests/responses/pagination' +import type { ITopic, ITopicOverview } from '../requests/responses/topic' +import type { PaginationResponse } from '../requests/responses/pagination' export function useThesis(thesisId: string | undefined) { const [thesis, setThesis] = useState() @@ -85,6 +85,7 @@ export function useApiPdfFile( }, ) } + // eslint-disable-next-line @eslint-react/exhaustive-deps -- onLoad is recreated by callers each render; refetching on its identity would loop }, [url, filename]) } diff --git a/client/src/hooks/local-storage.ts b/client/src/hooks/local-storage.ts index 11b77582b..f846bc406 100644 --- a/client/src/hooks/local-storage.ts +++ b/client/src/hooks/local-storage.ts @@ -1,4 +1,5 @@ -import { Dispatch, useCallback, useEffect, useMemo, useState } from 'react' +import type { Dispatch } from 'react' +import { useCallback, useEffect, useMemo, useState } from 'react' interface ILocalStorageOptions { usingJson: boolean @@ -53,6 +54,7 @@ export function useLocalStorage( } return undefined + // eslint-disable-next-line @eslint-react/exhaustive-deps -- `version` is intentional: it bumps via the storage event listener so the memo recomputes when the underlying storage value changes }, [key, version, usingJson, storage]) const setStoredValue = useCallback( diff --git a/client/src/hooks/notification.ts b/client/src/hooks/notification.ts index 1770821e2..174caaeca 100644 --- a/client/src/hooks/notification.ts +++ b/client/src/hooks/notification.ts @@ -1,4 +1,5 @@ -import { Dispatch, SetStateAction, useState } from 'react' +import type { Dispatch, SetStateAction } from 'react' +import { useState } from 'react' import { doRequest } from '../requests/request' import { showSimpleError } from '../utils/notification' import { getApiResponseErrorMessage } from '../requests/handler' diff --git a/client/src/hooks/utility.ts b/client/src/hooks/utility.ts index da2371e64..599274f83 100644 --- a/client/src/hooks/utility.ts +++ b/client/src/hooks/utility.ts @@ -1,4 +1,4 @@ -import { useMemo, useState } from 'react' +import { useMemo, useReducer } from 'react' interface IUseSignalReturnType { signal: Promise @@ -7,7 +7,10 @@ interface IUseSignalReturnType { } export function useSignal(): IUseSignalReturnType { - const [, setVersion] = useState(0) + // forceRender bumps a counter to force a re-render of the consuming + // component when the signal is triggered; the counter value itself is not + // exposed. + const [, forceRender] = useReducer((x: number) => x + 1, 0) return useMemo(() => { let externalResolve: (x: boolean) => unknown @@ -26,7 +29,7 @@ export function useSignal(): IUseSignalReturnType { externalResolve(true) ref.isTriggerred = true - setVersion((prev) => prev + 1) + forceRender() }, } }, []) diff --git a/client/src/index.tsx b/client/src/index.tsx index e9cf01f91..702b9b0fb 100644 --- a/client/src/index.tsx +++ b/client/src/index.tsx @@ -3,6 +3,10 @@ import App from './app/App' const rootElement = document.getElementById('root') -const root = createRoot(rootElement!) +if (!rootElement) { + throw new Error('Root element with id "root" not found in the document') +} + +const root = createRoot(rootElement) root.render() diff --git a/client/src/pages/AboutPage/AboutPage.tsx b/client/src/pages/AboutPage/AboutPage.tsx index ae884452f..ff55b5ccf 100644 --- a/client/src/pages/AboutPage/AboutPage.tsx +++ b/client/src/pages/AboutPage/AboutPage.tsx @@ -121,10 +121,10 @@ const AboutPage = () => { Git Information - Branch: {info?.git.branch || 'unknown'} + Branch: {info?.git.branch ?? 'unknown'} - Commit: {info?.git.commit.id || 'unknown'} + Commit: {info?.git.commit.id ?? 'unknown'} diff --git a/client/src/pages/AdminPage/AdminPage.tsx b/client/src/pages/AdminPage/AdminPage.tsx index a843efd9c..88cf042bf 100644 --- a/client/src/pages/AdminPage/AdminPage.tsx +++ b/client/src/pages/AdminPage/AdminPage.tsx @@ -15,7 +15,7 @@ import { Warning } from '@phosphor-icons/react' import { doRequest } from '../../requests/request' import { showSimpleError, showSimpleSuccess } from '../../utils/notification' import { getApiResponseErrorMessage } from '../../requests/handler' -import { IUser } from '../../requests/responses/user' +import type { IUser } from '../../requests/responses/user' interface IDataRetentionResult { deletedApplications: number @@ -186,7 +186,12 @@ const AdminPage = () => { that have exceeded the configured retention period. - @@ -201,7 +206,12 @@ const AdminPage = () => { while preserving the thesis record (title, type, grade, dates) for statistical purposes. - @@ -216,10 +226,17 @@ const AdminPage = () => { placeholder='Search by name, email, or ID...' value={searchQuery} onChange={(e) => setSearchQuery(e.currentTarget.value)} - onKeyDown={(e) => e.key === 'Enter' && onSearchUsers()} + onKeyDown={(e) => { + if (e.key === 'Enter') void onSearchUsers() + }} style={{ flex: 1 }} /> - @@ -229,7 +246,9 @@ const AdminPage = () => { - diff --git a/client/src/pages/BrowseThesesPage/components/CreateThesisModal/CreateThesisModal.tsx b/client/src/pages/BrowseThesesPage/components/CreateThesisModal/CreateThesisModal.tsx index 49c226ad5..eb9d89fe7 100644 --- a/client/src/pages/BrowseThesesPage/components/CreateThesisModal/CreateThesisModal.tsx +++ b/client/src/pages/BrowseThesesPage/components/CreateThesisModal/CreateThesisModal.tsx @@ -5,15 +5,15 @@ import React, { useEffect, useState } from 'react' import { UserMultiSelect } from '../../../../components/UserMultiSelect/UserMultiSelect' import { useNavigate } from 'react-router' import { doRequest } from '../../../../requests/request' -import { IThesis } from '../../../../requests/responses/thesis' +import type { IThesis } from '../../../../requests/responses/thesis' import { isNotEmptyUserList } from '../../../../utils/validation' import { showSimpleError } from '../../../../utils/notification' import { getApiResponseErrorMessage } from '../../../../requests/handler' import { formatThesisType, getDefaultLanguage } from '../../../../utils/format' import LanguageSelect from '../../../../components/LanguageSelect/LanguageSelect' -import { PaginationResponse } from '../../../../requests/responses/pagination' -import { ILightResearchGroup } from '../../../../requests/responses/researchGroup' -import { ILightUser } from '../../../../requests/responses/user' +import type { PaginationResponse } from '../../../../requests/responses/pagination' +import type { ILightResearchGroup } from '../../../../requests/responses/researchGroup' +import type { ILightUser } from '../../../../requests/responses/user' import { useHasGroupAccess } from '../../../../hooks/authentication' interface ICreateThesisModalProps { @@ -104,6 +104,7 @@ const CreateThesisModal = (props: ICreateThesisModalProps) => { setLoading(false) }, ) + // eslint-disable-next-line @eslint-react/exhaustive-deps -- form is stable; including it would loop on every form value change }, [opened]) return ( @@ -128,7 +129,7 @@ const CreateThesisModal = (props: ICreateThesisModalProps) => { }) if (response.ok) { - navigate(`/theses/${response.data.thesisId}`) + void navigate(`/theses/${response.data.thesisId}`) } else { showSimpleError(getApiResponseErrorMessage(response)) } diff --git a/client/src/pages/DashboardPage/DashboardPage.tsx b/client/src/pages/DashboardPage/DashboardPage.tsx index f670e53e4..ff94938d7 100644 --- a/client/src/pages/DashboardPage/DashboardPage.tsx +++ b/client/src/pages/DashboardPage/DashboardPage.tsx @@ -5,7 +5,8 @@ import ThesesTable from '../../components/ThesesTable/ThesesTable' import ApplicationsProvider from '../../providers/ApplicationsProvider/ApplicationsProvider' import ThesesProvider from '../../providers/ThesesProvider/ThesesProvider' import { Button, Center, Group, Stack, Title } from '@mantine/core' -import { ApplicationState, IApplication } from '../../requests/responses/application' +import type { IApplication } from '../../requests/responses/application' +import { ApplicationState } from '../../requests/responses/application' import ThesesGanttChart from '../../components/ThesesGanttChart/ThesesGanttChart' import { useManagementAccess } from '../../hooks/authentication' import { Link } from 'react-router' diff --git a/client/src/pages/DashboardPage/components/MyTasksSection/MyTasksSection.tsx b/client/src/pages/DashboardPage/components/MyTasksSection/MyTasksSection.tsx index 7db1fda7a..040ddc477 100644 --- a/client/src/pages/DashboardPage/components/MyTasksSection/MyTasksSection.tsx +++ b/client/src/pages/DashboardPage/components/MyTasksSection/MyTasksSection.tsx @@ -3,7 +3,7 @@ import React, { useEffect, useState } from 'react' import { doRequest } from '../../../../requests/request' import { showSimpleError } from '../../../../utils/notification' import { getApiResponseErrorMessage } from '../../../../requests/handler' -import { ITask } from '../../../../requests/responses/dashboard' +import type { ITask } from '../../../../requests/responses/dashboard' import { DataTable } from 'mantine-datatable' import { useNavigate } from 'react-router' import { Link as LinkIcon } from '@phosphor-icons/react' @@ -44,7 +44,7 @@ const MyTasksSection = () => { if (task.link.startsWith('http')) { window.location.replace(task.link) } else { - navigate(task.link) + void navigate(task.link) } } diff --git a/client/src/pages/ImprintPage/ImprintPage.tsx b/client/src/pages/ImprintPage/ImprintPage.tsx index 25193c2aa..3f25639b4 100644 --- a/client/src/pages/ImprintPage/ImprintPage.tsx +++ b/client/src/pages/ImprintPage/ImprintPage.tsx @@ -1,4 +1,5 @@ import { Title } from '@mantine/core' +import DOMPurify from 'dompurify' import { usePageTitle } from '../../hooks/theme' import { useEffect, useState } from 'react' @@ -8,15 +9,22 @@ const ImprintPage = () => { const [content, setContent] = useState('') useEffect(() => { - fetch('/imprint.html') + const controller = new AbortController() + fetch('/imprint.html', { signal: controller.signal }) .then((res) => res.text()) .then((res) => setContent(res)) + .catch((err: unknown) => { + if (err instanceof Error && err.name === 'AbortError') return + console.warn('Failed to load imprint content', err) + }) + return () => controller.abort() }, []) return (
Imprint -
+ {/* eslint-disable-next-line @eslint-react/dom-no-dangerously-set-innerhtml -- content is sanitized via DOMPurify on the line below */} +
) } diff --git a/client/src/pages/InterviewBookingPage/InterviewBookingPage.tsx b/client/src/pages/InterviewBookingPage/InterviewBookingPage.tsx index b3608877b..c5a2125ee 100644 --- a/client/src/pages/InterviewBookingPage/InterviewBookingPage.tsx +++ b/client/src/pages/InterviewBookingPage/InterviewBookingPage.tsx @@ -12,7 +12,7 @@ import { Paper, } from '@mantine/core' import { useIsSmallerBreakpoint } from '../../hooks/theme' -import { IInterviewSlot } from '../../requests/responses/interview' +import type { IInterviewSlot } from '../../requests/responses/interview' import { useEffect, useState } from 'react' import SummaryCard from './components/SummaryCard' import { @@ -29,12 +29,54 @@ import { useParams } from 'react-router' import { showSimpleError } from '../../utils/notification' import { getApiResponseErrorMessage } from '../../requests/handler' import { useAuthenticationContext, useUser } from '../../hooks/authentication' -import { ITopic } from '../../requests/responses/topic' +import type { ITopic } from '../../requests/responses/topic' import AvatarUserList from '../../components/AvatarUserList/AvatarUserList' import InterviewProcessProvider from '../../providers/InterviewProcessProvider/InterviewProcessProvider' import SelectSlotCarousel from './components/SelectSlotCarousel' import CancelSlotConfirmationModal from '../InterviewTopicOverviewPage/components/CancelSlotConfirmationModal' +interface ISlotInformationProps { + slot: IInterviewSlot + title?: string +} + +const SlotInformation = ({ slot, title }: ISlotInformationProps) => ( + + {slot.startDate.toLocaleDateString()} + + ), + icon: , + }, + { + title: 'Time', + content: ( + + {`${slot.startDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}` + + ` - ${slot.endDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}` + + `, ${Math.round((slot.endDate.getTime() - slot.startDate.getTime()) / 60000)} min`} + + ), + icon: , + }, + { + title: 'Location', + content: ( + + {slot.location ?? slot.streamUrl ?? 'Not specified'} + + ), + icon: , + }, + ]} + /> +) + const InterviewBookingPage = () => { const { processId } = useParams<{ processId: string }>() @@ -56,11 +98,12 @@ const InterviewBookingPage = () => { return () => clearInterval(interval) } + // eslint-disable-next-line @eslint-react/exhaustive-deps -- auth context is recreated each render; retriggering on identity would loop the login interval }, [auth.isAuthenticated]) const [topicInformation, setTopicInformation] = useState(null) - const fetchTopicInformation = async () => { + const fetchTopicInformation = () => { doRequest( `/v2/interview-process/${processId}/topic`, { @@ -80,10 +123,10 @@ const InterviewBookingPage = () => { const [pageLoading, setPageLoading] = useState(true) const [myBooking, setMyBooking] = useState(null) - const fetchMyBooking = async () => { + const fetchMyBooking = () => { setPageLoading(true) - new Promise((resolve) => { + void new Promise((resolve) => { doRequest( `/v2/interview-process/${processId}/completed`, { @@ -128,7 +171,7 @@ const InterviewBookingPage = () => { const [processCompleted, setProcessCompleted] = useState(false) - const bookSlot = async (slotId: string) => { + const bookSlot = (slotId: string) => { setPageLoading(true) doRequest( `/v2/interview-process/${processId}/slot/${slotId}/book`, @@ -159,6 +202,7 @@ const InterviewBookingPage = () => { fetchMyBooking() fetchTopicInformation() } + // eslint-disable-next-line @eslint-react/exhaustive-deps -- fetchMyBooking and fetchTopicInformation are recreated each render; effect should only re-run on processId/auth changes }, [processId, auth.isAuthenticated]) const [cancelModalOpen, setCancelModalOpen] = useState(false) @@ -199,43 +243,6 @@ const InterviewBookingPage = () => { /> ) : null - const SlotInformation = (slot: IInterviewSlot, title?: string) => ( - - {slot.startDate.toLocaleDateString()} - - ), - icon: , - }, - { - title: 'Time', - content: ( - - {`${slot.startDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}` + - ` - ${slot.endDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}` + - `, ${Math.round((slot.endDate.getTime() - slot.startDate.getTime()) / 60000)} min`} - - ), - icon: , - }, - { - title: 'Location', - content: ( - - {slot.location || slot.streamUrl || 'Not specified'} - - ), - icon: , - }, - ]} - > - ) - if (pageLoading) { return (
@@ -286,7 +293,7 @@ const InterviewBookingPage = () => { {TopicInformation} - {SlotInformation(myBooking)} + @@ -350,7 +357,7 @@ const InterviewBookingPage = () => { > - {selectedSlot && SlotInformation(selectedSlot)} + {selectedSlot && } {TopicInformation} diff --git a/client/src/pages/InterviewBookingPage/components/SelectSlotCarousel.tsx b/client/src/pages/InterviewBookingPage/components/SelectSlotCarousel.tsx index 652ae8e16..241671ac0 100644 --- a/client/src/pages/InterviewBookingPage/components/SelectSlotCarousel.tsx +++ b/client/src/pages/InterviewBookingPage/components/SelectSlotCarousel.tsx @@ -5,7 +5,7 @@ import { useIsSmallerBreakpoint } from '../../../hooks/theme' import { useInterviewProcessContext } from '../../../providers/InterviewProcessProvider/hooks' import SlotItem from '../../InterviewTopicOverviewPage/components/SlotItem' import { useState } from 'react' -import { IInterviewSlot } from '../../../requests/responses/interview' +import type { IInterviewSlot } from '../../../requests/responses/interview' interface ISelectSlotCarouselProps { selectedSlot: IInterviewSlot | null diff --git a/client/src/pages/InterviewOverviewPage/InterviewOverviewPage.tsx b/client/src/pages/InterviewOverviewPage/InterviewOverviewPage.tsx index 3a067c7bf..463875c1d 100644 --- a/client/src/pages/InterviewOverviewPage/InterviewOverviewPage.tsx +++ b/client/src/pages/InterviewOverviewPage/InterviewOverviewPage.tsx @@ -12,7 +12,7 @@ import { Pagination, } from '@mantine/core' import { ChatCircleSlashIcon, PlusIcon } from '@phosphor-icons/react' -import { IInterviewProcess, IUpcomingInterview } from '../../requests/responses/interview' +import type { IInterviewProcess, IUpcomingInterview } from '../../requests/responses/interview' import InterviewProcessCard from './components/InterviewProcessCard' import { useIsSmallerBreakpoint } from '../../hooks/theme' import UpcomingInterviewCard from './components/UpcomingInterviewCard' @@ -22,7 +22,7 @@ import { useEffect, useState } from 'react' import { showSimpleError } from '../../utils/notification' import { getApiResponseErrorMessage } from '../../requests/handler' import { doRequest } from '../../requests/request' -import { PaginationResponse } from '../../requests/responses/pagination' +import type { PaginationResponse } from '../../requests/responses/pagination' const InterviewOverviewPage = () => { const [upcomingInterviews, setUpcomingInterviews] = useState([]) @@ -39,7 +39,7 @@ const InterviewOverviewPage = () => { const [createModalOpened, setCreateModalOpened] = useState(false) - const fetchUpcomingInterviews = async () => { + const fetchUpcomingInterviews = () => { doRequest( `/v2/interview-process/upcoming-interviews`, { @@ -59,7 +59,7 @@ const InterviewOverviewPage = () => { ) } - const fetchMyInterviewProcesses = async () => { + const fetchMyInterviewProcesses = () => { setInterviewProcessesLoading(true) doRequest>( '/v2/interview-process', @@ -84,11 +84,13 @@ const InterviewOverviewPage = () => { useEffect(() => { fetchMyInterviewProcesses() + // eslint-disable-next-line @eslint-react/exhaustive-deps -- fetchMyInterviewProcesses is recreated each render; effect should only re-run on page change }, [page]) useEffect(() => { fetchMyInterviewProcesses() fetchUpcomingInterviews() + // eslint-disable-next-line @eslint-react/exhaustive-deps -- mount-only initial fetch }, []) return ( @@ -142,7 +144,7 @@ const InterviewOverviewPage = () => { key={`card-${process.interviewProcessId}`} interviewProcess={process} onClick={() => { - navigate(`/interviews/${process.interviewProcessId}`) + void navigate(`/interviews/${process.interviewProcessId}`) }} /> )) @@ -197,7 +199,7 @@ const InterviewOverviewPage = () => { key={interview.slot.bookedBy?.intervieweeId} upcomingInterview={interview} onClick={() => { - navigate( + void navigate( `/interviews/${interview.interviewProcessId}/interviewee/${interview.slot.bookedBy?.intervieweeId}`, ) }} diff --git a/client/src/pages/InterviewOverviewPage/components/CreateInterviewProcess.tsx b/client/src/pages/InterviewOverviewPage/components/CreateInterviewProcess.tsx index da6f74746..a5c97089f 100644 --- a/client/src/pages/InterviewOverviewPage/components/CreateInterviewProcess.tsx +++ b/client/src/pages/InterviewOverviewPage/components/CreateInterviewProcess.tsx @@ -15,8 +15,8 @@ import { useMantineColorScheme, } from '@mantine/core' import { useEffect, useState } from 'react' -import { PaginationResponse } from '../../../requests/responses/pagination' -import { +import type { PaginationResponse } from '../../../requests/responses/pagination' +import type { IApplicationInterviewProcess, IInterviewProcess, ITopicInterviewProcess, @@ -50,7 +50,7 @@ const CreateInterviewProcess = ({ opened, onClose }: CreateInterviewProcessProps const [selectedApplicants, setSelectedApplicants] = useState([]) - const fetchPossibleInterviewTopics = async () => { + const fetchPossibleInterviewTopics = () => { setTopicsLoading(true) doRequest>( '/v2/topics/interview-topics', @@ -72,7 +72,7 @@ const CreateInterviewProcess = ({ opened, onClose }: CreateInterviewProcessProps ) } - const fetchPossibleInterviewApplicantsByTopic = async () => { + const fetchPossibleInterviewApplicantsByTopic = () => { if (!selectedTopic) { setPossibleInterviewApplicants([]) return @@ -153,6 +153,7 @@ const CreateInterviewProcess = ({ opened, onClose }: CreateInterviewProcessProps useEffect(() => { fetchPossibleInterviewApplicantsByTopic() + // eslint-disable-next-line @eslint-react/exhaustive-deps -- fetchPossibleInterviewApplicantsByTopic is recreated each render by the provider; effect should only re-run when selectedTopic changes }, [selectedTopic]) useEffect(() => { diff --git a/client/src/pages/InterviewOverviewPage/components/InterviewProcessCard.tsx b/client/src/pages/InterviewOverviewPage/components/InterviewProcessCard.tsx index 8e6876fb7..545537413 100644 --- a/client/src/pages/InterviewOverviewPage/components/InterviewProcessCard.tsx +++ b/client/src/pages/InterviewOverviewPage/components/InterviewProcessCard.tsx @@ -11,7 +11,7 @@ import { Badge, useMantineColorScheme, } from '@mantine/core' -import { IInterviewProcess, InterviewState } from '../../../requests/responses/interview' +import type { IInterviewProcess, InterviewState } from '../../../requests/responses/interview' import { useHover } from '@mantine/hooks' import { getInterviewStateColor } from '../../../utils/format' @@ -80,7 +80,10 @@ const InterviewProcessCard = ({ interviewProcess, onClick }: IInterviewProcessCa @@ -105,7 +108,10 @@ const InterviewProcessCard = ({ interviewProcess, onClick }: IInterviewProcessCa ) })} diff --git a/client/src/pages/InterviewOverviewPage/components/SelectApplicantsList.tsx b/client/src/pages/InterviewOverviewPage/components/SelectApplicantsList.tsx index 86d314d19..bb73a6faf 100644 --- a/client/src/pages/InterviewOverviewPage/components/SelectApplicantsList.tsx +++ b/client/src/pages/InterviewOverviewPage/components/SelectApplicantsList.tsx @@ -10,7 +10,7 @@ import { } from '@mantine/core' import { CheckCircleIcon, CircleIcon } from '@phosphor-icons/react' import { ApplicationState } from '../../../requests/responses/application' -import { IApplicationInterviewProcess } from '../../../requests/responses/interview' +import type { IApplicationInterviewProcess } from '../../../requests/responses/interview' interface ISelectApplicantsListProps { possibleInterviewApplicants: IApplicationInterviewProcess[] diff --git a/client/src/pages/InterviewOverviewPage/components/SelectTopicInterviewProcessItem.tsx b/client/src/pages/InterviewOverviewPage/components/SelectTopicInterviewProcessItem.tsx index 65474223e..1ec26bff7 100644 --- a/client/src/pages/InterviewOverviewPage/components/SelectTopicInterviewProcessItem.tsx +++ b/client/src/pages/InterviewOverviewPage/components/SelectTopicInterviewProcessItem.tsx @@ -1,5 +1,5 @@ import { Badge, Divider, Group, Stack, Text, useMantineColorScheme } from '@mantine/core' -import { ITopicInterviewProcess } from '../../../requests/responses/interview' +import type { ITopicInterviewProcess } from '../../../requests/responses/interview' import { useHover } from '@mantine/hooks' interface ISelectTopicInterviewProcessItemProps { diff --git a/client/src/pages/InterviewOverviewPage/components/UpcomingInterviewCard.tsx b/client/src/pages/InterviewOverviewPage/components/UpcomingInterviewCard.tsx index a146f7534..396134917 100644 --- a/client/src/pages/InterviewOverviewPage/components/UpcomingInterviewCard.tsx +++ b/client/src/pages/InterviewOverviewPage/components/UpcomingInterviewCard.tsx @@ -1,6 +1,6 @@ import { Card, Group, Stack, Title, Text, Divider } from '@mantine/core' import { useHover } from '@mantine/hooks' -import { IUpcomingInterview } from '../../../requests/responses/interview' +import type { IUpcomingInterview } from '../../../requests/responses/interview' import { CustomAvatar } from '../../../components/CustomAvatar/CustomAvatar' import InterviewSlotInformation from '../../../components/InterviewSlotInformation/InterviewSlotInformation' diff --git a/client/src/pages/InterviewTopicOverviewPage/InterviewTopicOverviewPage.tsx b/client/src/pages/InterviewTopicOverviewPage/InterviewTopicOverviewPage.tsx index 04c1656e3..e5e962603 100644 --- a/client/src/pages/InterviewTopicOverviewPage/InterviewTopicOverviewPage.tsx +++ b/client/src/pages/InterviewTopicOverviewPage/InterviewTopicOverviewPage.tsx @@ -5,7 +5,7 @@ import InterviewProcessProvider from '../../providers/InterviewProcessProvider/I import { useParams } from 'react-router' import { doRequest } from '../../requests/request' import { useEffect, useState } from 'react' -import { IInterviewProcess } from '../../requests/responses/interview' +import type { IInterviewProcess } from '../../requests/responses/interview' const InterviewTopicOverviewPage = () => { const { processId } = useParams<{ processId: string }>() @@ -13,7 +13,7 @@ const InterviewTopicOverviewPage = () => { const [processCompleted, setProcessCompleted] = useState(null) const [title, setTitle] = useState(null) - const fetchInterviewProcess = async () => { + const fetchInterviewProcess = () => { doRequest( `/v2/interview-process/${processId}`, { @@ -33,6 +33,7 @@ const InterviewTopicOverviewPage = () => { useEffect(() => { fetchInterviewProcess() + // eslint-disable-next-line @eslint-react/exhaustive-deps -- mount-only initial fetch }, []) return processCompleted !== null ? ( diff --git a/client/src/pages/InterviewTopicOverviewPage/components/AcceptApplicantModal.tsx b/client/src/pages/InterviewTopicOverviewPage/components/AcceptApplicantModal.tsx index 14b283520..560f868bd 100644 --- a/client/src/pages/InterviewTopicOverviewPage/components/AcceptApplicantModal.tsx +++ b/client/src/pages/InterviewTopicOverviewPage/components/AcceptApplicantModal.tsx @@ -1,7 +1,7 @@ import { Loader, Modal, Title, Group, Button } from '@mantine/core' -import { IIntervieweeLightWithNextSlot } from '../../../requests/responses/interview' +import type { IIntervieweeLightWithNextSlot } from '../../../requests/responses/interview' import ApplicationReviewForm from '../../../components/ApplicationReviewForm/ApplicationReviewForm' -import { IApplication } from '../../../requests/responses/application' +import type { IApplication } from '../../../requests/responses/application' import { useEffect, useState } from 'react' import { useApplicationsContext } from '../../../providers/ApplicationsProvider/hooks' import ApplicationRejectButton from '../../../components/ApplicationRejectButton/ApplicationRejectButton' @@ -24,7 +24,8 @@ const AcceptApplicantModal = ({ interviewee, onAcceptSuccessfull }: AcceptApplic const app = await fetchApplication(interviewee.applicationId) setApplication(app) } - fetchData() + void fetchData() + // eslint-disable-next-line @eslint-react/exhaustive-deps -- fetchApplication is recreated each render by the provider; effect should only re-run when applicationId changes }, [interviewee.applicationId]) return ( diff --git a/client/src/pages/InterviewTopicOverviewPage/components/AddIntervieweesModal.tsx b/client/src/pages/InterviewTopicOverviewPage/components/AddIntervieweesModal.tsx index b16a07379..4ee0eeaec 100644 --- a/client/src/pages/InterviewTopicOverviewPage/components/AddIntervieweesModal.tsx +++ b/client/src/pages/InterviewTopicOverviewPage/components/AddIntervieweesModal.tsx @@ -11,10 +11,10 @@ import { Stack, } from '@mantine/core' import { useEffect, useState } from 'react' -import { IApplicationInterviewProcess } from '../../../requests/responses/interview' +import type { IApplicationInterviewProcess } from '../../../requests/responses/interview' import SelectApplicantsList from '../../InterviewOverviewPage/components/SelectApplicantsList' import { doRequest } from '../../../requests/request' -import { PaginationResponse } from '../../../requests/responses/pagination' +import type { PaginationResponse } from '../../../requests/responses/pagination' import { showSimpleError } from '../../../utils/notification' import { getApiResponseErrorMessage } from '../../../requests/handler' import { useParams } from 'react-router' @@ -37,7 +37,7 @@ const AddIntervieweesModal = ({ opened, closeModal }: IAddIntervieweesModalProps const { addIntervieweesToProcess } = useInterviewProcessContext() - const fetchPossibleInterviewApplicantsByTopic = async () => { + const fetchPossibleInterviewApplicantsByTopic = () => { setApplicantsLoading(true) doRequest>( `/v2/interview-process/${processId}/interview-applications`, @@ -63,6 +63,7 @@ const AddIntervieweesModal = ({ opened, closeModal }: IAddIntervieweesModalProps if (opened) { fetchPossibleInterviewApplicantsByTopic() } + // eslint-disable-next-line @eslint-react/exhaustive-deps -- fetchPossibleInterviewApplicantsByTopic is recreated each render; only re-run on modal open toggle }, [opened]) const onClose = () => { @@ -102,7 +103,7 @@ const AddIntervieweesModal = ({ opened, closeModal }: IAddIntervieweesModalProps
- {presentations && presentations.size === 0 && ( + {presentations?.size === 0 && ( No Presentations Scheduled @@ -346,6 +349,7 @@ const PresentationOverviewPage = () => { presentation={p} thesis={p.thesis} hasEditAccess={ + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- `false` must fall through to the next check user?.groups?.includes('admin') || user?.researchGroupId === p.thesis.researchGroup.id || (p.thesis.students ?? []).some( @@ -353,6 +357,7 @@ const PresentationOverviewPage = () => { ) } hasAcceptAccess={ + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- `false` must fall through to the next check user?.groups?.includes('admin') || user?.researchGroupId === p.thesis.researchGroup.id } @@ -360,7 +365,7 @@ const PresentationOverviewPage = () => { titleOrder={6} includeStudents={true} includeThesisStatus={true} - onClick={() => navigate(`/presentations/${p.presentationId}`)} + onClick={() => void navigate(`/presentations/${p.presentationId}`)} onDelete={() => { onDelete(p.presentationId, date) }} @@ -399,6 +404,7 @@ const PresentationOverviewPage = () => { presentation={p} thesis={p.thesis} hasEditAccess={ + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- `false` must fall through to the next check user?.groups?.includes('admin') || user?.researchGroupId === p.thesis.researchGroup.id || (p.thesis.students ?? []).some( @@ -406,6 +412,7 @@ const PresentationOverviewPage = () => { ) } hasAcceptAccess={ + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- `false` must fall through to the next check user?.groups?.includes('admin') || user?.researchGroupId === p.thesis.researchGroup.id } @@ -413,7 +420,7 @@ const PresentationOverviewPage = () => { includeThesisStatus={true} titleOrder={6} includeStudents={true} - onClick={() => navigate(`/presentations/${p.presentationId}`)} + onClick={() => void navigate(`/presentations/${p.presentationId}`)} onDelete={() => { onDelete(p.presentationId, date) }} diff --git a/client/src/pages/PresentationOverviewPage/wheelGuard.test.ts b/client/src/pages/PresentationOverviewPage/wheelGuard.test.ts index ed850c500..b02b3be83 100644 --- a/client/src/pages/PresentationOverviewPage/wheelGuard.test.ts +++ b/client/src/pages/PresentationOverviewPage/wheelGuard.test.ts @@ -18,6 +18,9 @@ describe('PresentationOverviewPage — issue #729 (mobile scroll guard)', () => /addEventListener\(['"]wheel['"][\s\S]*?removeEventListener\(['"]wheel['"][\s\S]*?\}\s*,\s*\[([^\]]*)\]\s*\)/ const match = source.match(depRegex) expect(match).not.toBeNull() - expect(match![1]).toMatch(/isSmaller/) + if (!match) { + throw new Error('expected wheel useEffect to match dep regex') + } + expect(match[1]).toMatch(/isSmaller/) }) }) diff --git a/client/src/pages/PresentationPage/PresentationPage.tsx b/client/src/pages/PresentationPage/PresentationPage.tsx index 1b7f4f559..542343191 100644 --- a/client/src/pages/PresentationPage/PresentationPage.tsx +++ b/client/src/pages/PresentationPage/PresentationPage.tsx @@ -1,6 +1,6 @@ import { Link, useParams } from 'react-router' import React, { useEffect, useState } from 'react' -import { IPublishedPresentation } from '../../requests/responses/thesis' +import type { IPublishedPresentation } from '../../requests/responses/thesis' import { doRequest } from '../../requests/request' import ThesisData from '../../components/ThesisData/ThesisData' import { showSimpleError } from '../../utils/notification' @@ -62,7 +62,7 @@ const PresentationPage = () => { - + { - {(user?.groups?.includes('admin') || - user?.researchGroupId === presentation.thesis.researchGroup.id || - (presentation.thesis.students ?? []).some( - (student) => student.userId === user?.userId, - )) && ( - - )} + { + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- `false` must fall through to the next check + (user?.groups?.includes('admin') || + user?.researchGroupId === presentation.thesis.researchGroup.id || + (presentation.thesis.students ?? []).some( + (student) => student.userId === user?.userId, + )) && ( + + ) + } ) } diff --git a/client/src/pages/PrivacyPage/PrivacyPage.tsx b/client/src/pages/PrivacyPage/PrivacyPage.tsx index af3987909..056e15550 100644 --- a/client/src/pages/PrivacyPage/PrivacyPage.tsx +++ b/client/src/pages/PrivacyPage/PrivacyPage.tsx @@ -1,4 +1,5 @@ import { Anchor, Text, Title } from '@mantine/core' +import DOMPurify from 'dompurify' import { usePageTitle } from '../../hooks/theme' import { useEffect, useState } from 'react' import { useAuthenticationContext } from '../../hooks/authentication' @@ -11,15 +12,22 @@ const PrivacyPage = () => { const auth = useAuthenticationContext() useEffect(() => { - fetch('/privacy.html') + const controller = new AbortController() + fetch('/privacy.html', { signal: controller.signal }) .then((res) => res.text()) .then((res) => setContent(res)) + .catch((err: unknown) => { + if (err instanceof Error && err.name === 'AbortError') return + console.warn('Failed to load privacy content', err) + }) + return () => controller.abort() }, []) return (
Privacy -
+ {/* eslint-disable-next-line @eslint-react/dom-no-dangerously-set-innerhtml -- content is sanitized via DOMPurify on the line below */} +
{auth.isAuthenticated && (
diff --git a/client/src/pages/ReplaceApplicationPage/ReplaceApplicationPage.tsx b/client/src/pages/ReplaceApplicationPage/ReplaceApplicationPage.tsx index b641fdf14..791a7c6a4 100644 --- a/client/src/pages/ReplaceApplicationPage/ReplaceApplicationPage.tsx +++ b/client/src/pages/ReplaceApplicationPage/ReplaceApplicationPage.tsx @@ -5,7 +5,7 @@ import { useEffect, useState } from 'react' import SelectTopicStep from './components/SelectTopicStep/SelectTopicStep' import StudentInformationStep from './components/StudentInformationStep/StudentInformationStep' import MotivationStep from './components/MotivationStep/MotivationStep' -import { IApplication } from '../../requests/responses/application' +import type { IApplication } from '../../requests/responses/application' import { doRequest } from '../../requests/request' import { usePageTitle } from '../../hooks/theme' @@ -47,7 +47,7 @@ const ReplaceApplicationPage = () => { } if (value === 0 && topicId) { - navigate(`/submit-application`, { replace: true }) + void navigate(`/submit-application`, { replace: true }) } window.scrollTo(0, 0) @@ -61,7 +61,7 @@ const ReplaceApplicationPage = () => { <Stepper.Step label='First Step' description='Select Topic'> <SelectTopicStep onComplete={(x) => { - navigate(`/submit-application/${x?.topicId || ''}`, { replace: true }) + void navigate(`/submit-application/${x?.topicId ?? ''}`, { replace: true }) setStep(1) }} /> @@ -77,6 +77,7 @@ const ReplaceApplicationPage = () => { <Stepper.Step label='Final step' description='Submit your Application'> <MotivationStep onComplete={() => setStep(3)} + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- useTopic may return `false` (error sentinel) which must also map to undefined topic={topic || undefined} application={application} consentToPrivacyPolicy={consentToPrivacyPolicy} diff --git a/client/src/pages/ReplaceApplicationPage/components/MotivationStep/MotivationStep.tsx b/client/src/pages/ReplaceApplicationPage/components/MotivationStep/MotivationStep.tsx index bf92ae443..95eb191c2 100644 --- a/client/src/pages/ReplaceApplicationPage/components/MotivationStep/MotivationStep.tsx +++ b/client/src/pages/ReplaceApplicationPage/components/MotivationStep/MotivationStep.tsx @@ -1,4 +1,4 @@ -import { ITopic } from '../../../../requests/responses/topic' +import type { ITopic } from '../../../../requests/responses/topic' import { isNotEmpty, useForm } from '@mantine/form' import { Accordion, Button, Select, Stack, TextInput } from '@mantine/core' import DocumentEditor from '../../../../components/DocumentEditor/DocumentEditor' @@ -9,11 +9,11 @@ import { getApiResponseErrorMessage } from '../../../../requests/handler' import { DateInput } from '@mantine/dates' import { getHtmlTextLength } from '../../../../utils/validation' import { GLOBAL_CONFIG } from '../../../../config/global' -import { IApplication } from '../../../../requests/responses/application' +import type { IApplication } from '../../../../requests/responses/application' import TopicAccordionItem from '../../../../components/TopicAccordionItem/TopicAccordionItem' import { formatThesisType } from '../../../../utils/format' -import { PaginationResponse } from '../../../../requests/responses/pagination' -import { ILightResearchGroup } from '../../../../requests/responses/researchGroup' +import type { PaginationResponse } from '../../../../requests/responses/pagination' +import type { ILightResearchGroup } from '../../../../requests/responses/researchGroup' interface IMotivationStepProps { topic: ITopic | undefined @@ -36,7 +36,7 @@ const MotivationStep = (props: IMotivationStepProps) => { const [researchGroups, setResearchGroups] = useState<PaginationResponse<ILightResearchGroup>>() const [loading, setLoading] = useState(false) - const mergedTopic = application?.topic || topic + const mergedTopic = application?.topic ?? topic const form = useForm<IMotivationStepForm>({ mode: 'controlled', @@ -77,6 +77,7 @@ const MotivationStep = (props: IMotivationStepProps) => { researchGroupId: application.researchGroup.id, }) } + // eslint-disable-next-line @eslint-react/exhaustive-deps -- form is stable and `application` itself is captured via applicationId; rerunning on full application identity would loop }, [application?.applicationId]) useEffect(() => { @@ -129,6 +130,7 @@ const MotivationStep = (props: IMotivationStepProps) => { setLoading(false) }, ) + // eslint-disable-next-line @eslint-react/exhaustive-deps -- form is stable; only re-run when the merged topic changes }, [mergedTopic]) const onSubmit = async (values: IMotivationStepForm) => { @@ -183,7 +185,7 @@ const MotivationStep = (props: IMotivationStepProps) => { label='Research Group' required nothingFoundMessage={!loading ? 'Nothing found...' : 'Loading...'} - disabled={!!mergedTopic} + disabled={Boolean(mergedTopic)} data={(researchGroups?.content ?? []).map((researchGroup: ILightResearchGroup) => ({ label: researchGroup.name, value: researchGroup.id, @@ -193,7 +195,7 @@ const MotivationStep = (props: IMotivationStepProps) => { <Select label='Thesis Type' required={true} - data={(mergedTopic?.thesisTypes || Object.keys(GLOBAL_CONFIG.thesis_types)).map( + data={(mergedTopic?.thesisTypes ?? Object.keys(GLOBAL_CONFIG.thesis_types)).map( (thesisType) => ({ label: formatThesisType(thesisType), value: thesisType, diff --git a/client/src/pages/ReplaceApplicationPage/components/SelectTopicStep/SelectTopicStep.tsx b/client/src/pages/ReplaceApplicationPage/components/SelectTopicStep/SelectTopicStep.tsx index f0a21787c..3443dce44 100644 --- a/client/src/pages/ReplaceApplicationPage/components/SelectTopicStep/SelectTopicStep.tsx +++ b/client/src/pages/ReplaceApplicationPage/components/SelectTopicStep/SelectTopicStep.tsx @@ -1,4 +1,4 @@ -import { ITopic } from '../../../../requests/responses/topic' +import type { ITopic } from '../../../../requests/responses/topic' import { Alert, Stack } from '@mantine/core' import { InfoIcon } from '@phosphor-icons/react' import React, { useEffect, useState } from 'react' @@ -9,7 +9,7 @@ import { useDebouncedValue } from '@mantine/hooks' import TopicsProvider from '../../../../providers/TopicsProvider/TopicsProvider' import TopicSearchFilters from '../../../../components/TopicSearchFilters/TopicSearchFilters' import { doRequest } from '../../../../requests/request' -import { ILightResearchGroup } from '../../../../requests/responses/researchGroup' +import type { ILightResearchGroup } from '../../../../requests/responses/researchGroup' import { showSimpleError } from '../../../../utils/notification' import { getApiResponseErrorMessage } from '../../../../requests/handler' import TopicCardGrid from '../../../LandingPage/components/TopicCardGrid/TopicCardGrid' @@ -62,6 +62,7 @@ const SelectTopicStep = (props: ISelectTopicStepProps) => { params.delete('search') } setSearchParams(params, { replace: true }) + // eslint-disable-next-line @eslint-react/exhaustive-deps -- searchParams/setSearchParams change on every navigation; only sync URL when the debounced search input changes }, [debouncedSearch]) return ( diff --git a/client/src/pages/ReplaceApplicationPage/components/SelectTopicStep/components/CollapsibleTopicElement.tsx b/client/src/pages/ReplaceApplicationPage/components/SelectTopicStep/components/CollapsibleTopicElement.tsx index cabb3a4a7..7412d1f56 100644 --- a/client/src/pages/ReplaceApplicationPage/components/SelectTopicStep/components/CollapsibleTopicElement.tsx +++ b/client/src/pages/ReplaceApplicationPage/components/SelectTopicStep/components/CollapsibleTopicElement.tsx @@ -11,9 +11,9 @@ import { Center, Loader, } from '@mantine/core' -import { ITopicOverview, ITopic } from '../../../../../requests/responses/topic' +import type { ITopicOverview, ITopic } from '../../../../../requests/responses/topic' import ThesisTypeBadge from '../../../../LandingPage/components/ThesisTypBadge/ThesisTypBadge' -import { IPublishedThesis } from '../../../../../requests/responses/thesis' +import type { IPublishedThesis } from '../../../../../requests/responses/thesis' import { useHover } from '@mantine/hooks' import AvatarUserList from '../../../../../components/AvatarUserList/AvatarUserList' import DocumentEditor from '../../../../../components/DocumentEditor/DocumentEditor' @@ -34,6 +34,11 @@ const CollapsibleTopicElement = ({ topic, onApply }: ICollapsibleTopicElementPro const isTopicOverview = 'topicId' in topic const fullTopic = useTopic(isTopicOverview && expanded ? topic.topicId : undefined) + const deadlinePassed = + !!fullTopic && + !!fullTopic.applicationDeadline && + new Date(fullTopic.applicationDeadline) < new Date() + return ( <Card withBorder @@ -146,30 +151,23 @@ const CollapsibleTopicElement = ({ topic, onApply }: ICollapsibleTopicElementPro ) : ( <></> )} - {onApply && - isTopicOverview && - (() => { - const deadlinePassed = - !!fullTopic && - !!fullTopic.applicationDeadline && - new Date(fullTopic.applicationDeadline) < new Date() - return ( - <Stack gap='xs'> - <Button - onClick={() => onApply(fullTopic || undefined)} - fullWidth - disabled={!fullTopic || deadlinePassed} - > - Apply - </Button> - {deadlinePassed && ( - <Text size='xs' c='dimmed' ta='center'> - Application deadline has passed. - </Text> - )} - </Stack> - ) - })()} + {onApply && isTopicOverview && ( + <Stack gap='xs'> + <Button + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- useTopic may return `false` (error sentinel) which must also map to undefined + onClick={() => onApply(fullTopic || undefined)} + fullWidth + disabled={!fullTopic || deadlinePassed} + > + Apply + </Button> + {deadlinePassed && ( + <Text size='xs' c='dimmed' ta='center'> + Application deadline has passed. + </Text> + )} + </Stack> + )} </Stack> </Accordion.Panel> </Accordion.Item> diff --git a/client/src/pages/ResearchGroupAdminPage/ResearchGroupAdminPage.tsx b/client/src/pages/ResearchGroupAdminPage/ResearchGroupAdminPage.tsx index 6ccb56928..41b2b1593 100644 --- a/client/src/pages/ResearchGroupAdminPage/ResearchGroupAdminPage.tsx +++ b/client/src/pages/ResearchGroupAdminPage/ResearchGroupAdminPage.tsx @@ -14,14 +14,13 @@ import { } from '@mantine/core' import { MagnifyingGlass, Plus, UsersThree } from '@phosphor-icons/react' import { doRequest } from '../../requests/request' -import { PaginationResponse } from '../../requests/responses/pagination' -import { IResearchGroup } from '../../requests/responses/researchGroup' +import type { PaginationResponse } from '../../requests/responses/pagination' +import type { IResearchGroup } from '../../requests/responses/researchGroup' import { showSimpleError } from '../../utils/notification' import { getApiResponseErrorMessage } from '../../requests/handler' import ResearchGroupCard from './components/ResearchGroupCard' -import CreateResearchGroupModal, { - ResearchGroupFormValues, -} from './components/CreateResearchGroupModal' +import type { ResearchGroupFormValues } from './components/CreateResearchGroupModal' +import CreateResearchGroupModal from './components/CreateResearchGroupModal' import { useDebouncedValue } from '@mantine/hooks' import { showNotification } from '@mantine/notifications' import NoContentFoundCard from '../../components/NoContentFoundCard/NoContentFoundCard' @@ -80,9 +79,10 @@ const ResearchGroupAdminPage = () => { useEffect(() => { fetchResearchGroups(0) + // eslint-disable-next-line @eslint-react/exhaustive-deps -- fetchResearchGroups is recreated each render; only re-run on debounced search change }, [debouncedSearch]) - const handleCreateResearchGroup = async (values: ResearchGroupFormValues) => { + const handleCreateResearchGroup = (values: ResearchGroupFormValues) => { const body = { headUsername: values.headUsername, name: values.name, @@ -163,7 +163,7 @@ const ResearchGroupAdminPage = () => { verticalSpacing={{ base: 'xs', sm: 'sm', xl: 'md' }} > {(researchGroups?.content ?? []).map((group) => ( - <ResearchGroupCard {...group} key={group.id} /> + <ResearchGroupCard key={group.id} {...group} /> ))} </SimpleGrid> )} diff --git a/client/src/pages/ResearchGroupAdminPage/components/ResearchGroupCard.tsx b/client/src/pages/ResearchGroupAdminPage/components/ResearchGroupCard.tsx index 2267dde7e..4be1827c2 100644 --- a/client/src/pages/ResearchGroupAdminPage/components/ResearchGroupCard.tsx +++ b/client/src/pages/ResearchGroupAdminPage/components/ResearchGroupCard.tsx @@ -1,6 +1,6 @@ import { Badge, Box, Card, Flex, Group, Stack, Text } from '@mantine/core' import { Buildings, Users } from '@phosphor-icons/react' -import { IResearchGroup } from '../../../requests/responses/researchGroup' +import type { IResearchGroup } from '../../../requests/responses/researchGroup' import { CustomAvatar } from '../../../components/CustomAvatar/CustomAvatar' import { formatUser } from '../../../utils/format' import { useHover } from '@mantine/hooks' diff --git a/client/src/pages/ResearchGroupSettingPage/ResearchGroupSettingPage.tsx b/client/src/pages/ResearchGroupSettingPage/ResearchGroupSettingPage.tsx index 2416c86fe..fd4cd3af0 100644 --- a/client/src/pages/ResearchGroupSettingPage/ResearchGroupSettingPage.tsx +++ b/client/src/pages/ResearchGroupSettingPage/ResearchGroupSettingPage.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react' import { useParams, useSearchParams } from 'react-router' -import { IResearchGroup } from '../../requests/responses/researchGroup' +import type { IResearchGroup } from '../../requests/responses/researchGroup' import { doRequest } from '../../requests/request' import { showSimpleError } from '../../utils/notification' import { getApiResponseErrorMessage } from '../../requests/handler' @@ -10,7 +10,7 @@ import ResearchGroupMembers from './components/MemberSettings/ResearchGroupMembe import EmailTemplatesOverview from './components/EmailTemplateSettings/EmailTemplatesOverview' import { useUser } from '../../hooks/authentication' import ApplicationPhaseSettingsCard from './components/ApplicationPhaseSettingsCard' -import { IResearchGroupSettings } from '../../requests/responses/researchGroupSettings' +import type { IResearchGroupSettings } from '../../requests/responses/researchGroupSettings' import PresentationSettingsCard from './components/PresentationSettingsCard' import ProposalSettingsCard from './components/ProposalSettingsCard' import EmailSettingsCard from './components/EmailSettingsCard' @@ -37,13 +37,14 @@ const ResearchGroupSettingPage = () => { useEffect(() => { const params = new URLSearchParams(searchParams) - if (selectedTab != 'general') { + if (selectedTab !== 'general') { params.set('setting', selectedTab) } else { params.delete('setting') } setSearchParams(params, { replace: true }) + // eslint-disable-next-line @eslint-react/exhaustive-deps -- searchParams/setSearchParams change on every navigation; only sync URL when the selected tab changes }, [selectedTab]) useEffect(() => { @@ -101,7 +102,7 @@ const ResearchGroupSettingPage = () => { <Tabs value={selectedTab} onChange={(value) => { - setSelectedTab(value || 'general') + setSelectedTab(value ?? 'general') }} > <Tabs.List> @@ -131,10 +132,10 @@ const ResearchGroupSettingPage = () => { <> <ApplicationPhaseSettingsCard automaticRejectionEnabledSettings={ - researchGroupSettings?.rejectSettings.automaticRejectEnabled || false + researchGroupSettings?.rejectSettings.automaticRejectEnabled ?? false } rejectDurationSettings={ - researchGroupSettings?.rejectSettings.rejectDuration || 8 + researchGroupSettings?.rejectSettings.rejectDuration ?? 8 } setAutomaticRejectionEnabledSettings={(value: boolean) => setResearchGroupSettings( @@ -163,7 +164,7 @@ const ResearchGroupSettingPage = () => { /> <ProposalSettingsCard proposalPhaseActive={ - researchGroupSettings?.phaseSettings.proposalPhaseActive || false + researchGroupSettings?.phaseSettings.proposalPhaseActive ?? false } setProposalPhaseActive={(value: boolean) => setResearchGroupSettings( @@ -180,7 +181,7 @@ const ResearchGroupSettingPage = () => { /> <PresentationSettingsCard presentationDurationSettings={ - researchGroupSettings?.presentationSettings.presentationSlotDuration || 30 + researchGroupSettings?.presentationSettings.presentationSlotDuration ?? 30 } setPresentationDurationSettings={(value: number) => setResearchGroupSettings( diff --git a/client/src/pages/ResearchGroupSettingPage/components/ApplicationEmailContentSettingsCard.tsx b/client/src/pages/ResearchGroupSettingPage/components/ApplicationEmailContentSettingsCard.tsx index f698ad2ee..5f05b4b52 100644 --- a/client/src/pages/ResearchGroupSettingPage/components/ApplicationEmailContentSettingsCard.tsx +++ b/client/src/pages/ResearchGroupSettingPage/components/ApplicationEmailContentSettingsCard.tsx @@ -4,7 +4,7 @@ import { doRequest } from '../../../requests/request' import { useParams } from 'react-router' import { showSimpleError } from '../../../utils/notification' import { getApiResponseErrorMessage } from '../../../requests/handler' -import { IResearchGroupSettings } from '../../../requests/responses/researchGroupSettings' +import type { IResearchGroupSettings } from '../../../requests/responses/researchGroupSettings' import { Info } from '@phosphor-icons/react' interface ApplicationEmailContentSettingsCardProps { diff --git a/client/src/pages/ResearchGroupSettingPage/components/ApplicationPhaseSettingsCard.tsx b/client/src/pages/ResearchGroupSettingPage/components/ApplicationPhaseSettingsCard.tsx index 0c628fb85..a2a99a8d2 100644 --- a/client/src/pages/ResearchGroupSettingPage/components/ApplicationPhaseSettingsCard.tsx +++ b/client/src/pages/ResearchGroupSettingPage/components/ApplicationPhaseSettingsCard.tsx @@ -4,7 +4,7 @@ import { doRequest } from '../../../requests/request' import { useParams } from 'react-router' import { showSimpleError } from '../../../utils/notification' import { getApiResponseErrorMessage } from '../../../requests/handler' -import { IResearchGroupSettings } from '../../../requests/responses/researchGroupSettings' +import type { IResearchGroupSettings } from '../../../requests/responses/researchGroupSettings' import { WarningCircleIcon } from '@phosphor-icons/react' interface ApplicationPhaseSettingsCard { diff --git a/client/src/pages/ResearchGroupSettingPage/components/EmailSettingsCard.tsx b/client/src/pages/ResearchGroupSettingPage/components/EmailSettingsCard.tsx index 8c2317155..3cd4f4a0e 100644 --- a/client/src/pages/ResearchGroupSettingPage/components/EmailSettingsCard.tsx +++ b/client/src/pages/ResearchGroupSettingPage/components/EmailSettingsCard.tsx @@ -7,7 +7,7 @@ import { getApiResponseErrorMessage } from '../../../requests/handler' import { showSimpleError } from '../../../utils/notification' import { showNotification } from '@mantine/notifications' import { useParams } from 'react-router' -import { +import type { IResearchGroupSettings, IResearchGroupSettingsEmail, } from '../../../requests/responses/researchGroupSettings' @@ -42,6 +42,7 @@ const EmailSettingsCard = ({ form.setValues({ applicationNotificationEmail: researchgroupEmailSettings?.applicationNotificationEmail ?? '', }) + // eslint-disable-next-line @eslint-react/exhaustive-deps -- form is stable; only re-sync when the server-provided email value changes }, [researchgroupEmailSettings?.applicationNotificationEmail]) const hasChanges = diff --git a/client/src/pages/ResearchGroupSettingPage/components/EmailTemplateSettings/EmailTemplateCard.tsx b/client/src/pages/ResearchGroupSettingPage/components/EmailTemplateSettings/EmailTemplateCard.tsx index 3e940eaeb..0bbddebb8 100644 --- a/client/src/pages/ResearchGroupSettingPage/components/EmailTemplateSettings/EmailTemplateCard.tsx +++ b/client/src/pages/ResearchGroupSettingPage/components/EmailTemplateSettings/EmailTemplateCard.tsx @@ -1,5 +1,5 @@ import { Badge, Card, Text, Stack, Title, Group, Button, Flex, Tooltip } from '@mantine/core' -import { IEmailTemplate } from '../../../../requests/responses/emailtemplate' +import type { IEmailTemplate } from '../../../../requests/responses/emailtemplate' import { useState } from 'react' import EmailTemplatePreviewModal from './EmailTemplatePreviewModal' import { EyeIcon, NotePencilIcon } from '@phosphor-icons/react' @@ -26,7 +26,7 @@ const EmailTemplateCard = ({ const navigateToEditPage = () => { if (!researchGroupId || !activeTemplate) return - navigate( + void navigate( `/research-groups/${researchGroupId}/email-templates/${activeTemplate.templateCase}/edit`, ) } diff --git a/client/src/pages/ResearchGroupSettingPage/components/EmailTemplateSettings/EmailTemplateEditPage.tsx b/client/src/pages/ResearchGroupSettingPage/components/EmailTemplateSettings/EmailTemplateEditPage.tsx index e91f6c937..3e0af96a6 100644 --- a/client/src/pages/ResearchGroupSettingPage/components/EmailTemplateSettings/EmailTemplateEditPage.tsx +++ b/client/src/pages/ResearchGroupSettingPage/components/EmailTemplateSettings/EmailTemplateEditPage.tsx @@ -4,8 +4,8 @@ import { useEffect, useState } from 'react' import { useNavigate, useParams } from 'react-router' import { getApiResponseErrorMessage } from '../../../../requests/handler' import { doRequest } from '../../../../requests/request' -import { IEmailTemplate } from '../../../../requests/responses/emailtemplate' -import { PaginationResponse } from '../../../../requests/responses/pagination' +import type { IEmailTemplate } from '../../../../requests/responses/emailtemplate' +import type { PaginationResponse } from '../../../../requests/responses/pagination' import { showSimpleError, showSimpleSuccess } from '../../../../utils/notification' import EmailTextEditor from './EmailTextEditor/EmailTextEditor' import DOMPurify from 'dompurify' @@ -59,11 +59,11 @@ const EmailTemplateEditPage = () => { ) }, [templateCase, researchGroupId]) - const deleteTemplate = async () => { + const deleteTemplate = () => { if (!researchGroupTemplate) return setLoading(true) - await doRequest( + doRequest( `/v2/email-templates/${researchGroupTemplate.id}`, { method: 'DELETE', @@ -82,7 +82,7 @@ const EmailTemplateEditPage = () => { ) } - const saveChanges = async () => { + const saveChanges = () => { if (!editingTemplate) return if (editingTemplate === defaultTemplate) { @@ -99,7 +99,7 @@ const EmailTemplateEditPage = () => { ? editingTemplate.researchGroup.id : researchGroupId - await doRequest<IEmailTemplate>( + doRequest<IEmailTemplate>( url, { method, @@ -143,7 +143,7 @@ const EmailTemplateEditPage = () => { cursor: 'pointer', }, }} - onClick={() => navigate(`/research-groups/${researchGroupId}?setting=email-templates`)} + onClick={() => void navigate(`/research-groups/${researchGroupId}?setting=email-templates`)} > <Group align='center'> <ArrowLeftIcon size={32} weight='bold' /> @@ -189,6 +189,7 @@ const EmailTemplateEditPage = () => { <Paper withBorder radius='sm' flex={1}> <div style={{ padding: '1rem' }} + // eslint-disable-next-line @eslint-react/dom-no-dangerously-set-innerhtml -- content sanitized via DOMPurify on the line below dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(exampleText) }} /> </Paper> diff --git a/client/src/pages/ResearchGroupSettingPage/components/EmailTemplateSettings/EmailTemplatePreviewModal.tsx b/client/src/pages/ResearchGroupSettingPage/components/EmailTemplateSettings/EmailTemplatePreviewModal.tsx index f11a01845..92a8884e5 100644 --- a/client/src/pages/ResearchGroupSettingPage/components/EmailTemplateSettings/EmailTemplatePreviewModal.tsx +++ b/client/src/pages/ResearchGroupSettingPage/components/EmailTemplateSettings/EmailTemplatePreviewModal.tsx @@ -3,7 +3,7 @@ import { useEffect, useMemo, useState } from 'react' import DOMPurify from 'dompurify' import { getApiResponseErrorMessage } from '../../../../requests/handler' import { doRequest } from '../../../../requests/request' -import { IEmailTemplate, IMailVariableDto } from '../../../../requests/responses/emailtemplate' +import type { IEmailTemplate, IMailVariableDto } from '../../../../requests/responses/emailtemplate' import { showSimpleError } from '../../../../utils/notification' interface IEmailTemplatePreviewModalProps { @@ -61,6 +61,7 @@ const EmailTemplatePreviewModal = ({ <Paper withBorder radius='sm'> <Box p='md'> + {/* eslint-disable-next-line @eslint-react/dom-no-dangerously-set-innerhtml -- content is sanitized via DOMPurify on the line below */} <div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(previewHtml) }} /> </Box> </Paper> diff --git a/client/src/pages/ResearchGroupSettingPage/components/EmailTemplateSettings/EmailTemplatesOverview.tsx b/client/src/pages/ResearchGroupSettingPage/components/EmailTemplateSettings/EmailTemplatesOverview.tsx index 9c1f8e0bc..e32cdb79c 100644 --- a/client/src/pages/ResearchGroupSettingPage/components/EmailTemplateSettings/EmailTemplatesOverview.tsx +++ b/client/src/pages/ResearchGroupSettingPage/components/EmailTemplateSettings/EmailTemplatesOverview.tsx @@ -1,8 +1,8 @@ import { useEffect, useState } from 'react' import { getApiResponseErrorMessage } from '../../../../requests/handler' import { doRequest } from '../../../../requests/request' -import { IEmailTemplate } from '../../../../requests/responses/emailtemplate' -import { PaginationResponse } from '../../../../requests/responses/pagination' +import type { IEmailTemplate } from '../../../../requests/responses/emailtemplate' +import type { PaginationResponse } from '../../../../requests/responses/pagination' import { showSimpleError } from '../../../../utils/notification' import { Box, Divider, Flex, Loader, Stack, TextInput, Title } from '@mantine/core' import { ResearchGroupSettingsCard } from '../ResearchGroupSettingsCard' @@ -14,6 +14,21 @@ interface EmailTemplatesOverviewProps { includeApplicationDataInEmail?: boolean } +const TEMPLATE_CASES_TO_FETCH: string[] = [ + 'APPLICATION_CREATED_CHAIR', + 'THESIS_PRESENTATION_INVITATION_UPDATED', + 'THESIS_PRESENTATION_INVITATION', + 'THESIS_PRESENTATION_INVITATION_CANCELLED', + 'APPLICATION_REJECTED_TOPIC_REQUIREMENTS', + 'APPLICATION_REJECTED_TOPIC_OUTDATED', + 'APPLICATION_REJECTED', + 'APPLICATION_REJECTED_TITLE_NOT_INTERESTING', + 'APPLICATION_REJECTED_STUDENT_REQUIREMENTS', + 'APPLICATION_REJECTED_TOPIC_FILLED', + 'APPLICATION_ACCEPTED', + 'APPLICATION_ACCEPTED_NO_SUPERVISOR', +] + const EmailTemplatesOverview = ({ includeApplicationDataInEmail = true, }: EmailTemplatesOverviewProps) => { @@ -73,10 +88,12 @@ const EmailTemplatesOverview = ({ const lowerKey = key.toLowerCase() return ( + /* eslint-disable @typescript-eslint/prefer-nullish-coalescing -- `false` must fall through to the next check across optional fields */ template.default?.description.toLowerCase().includes(lowerKey) || template.default?.subject.toLowerCase().includes(lowerKey) || template.default?.templateCase.toLowerCase().includes(lowerKey) || category.toLowerCase().includes(lowerKey) + /* eslint-enable @typescript-eslint/prefer-nullish-coalescing */ ) } @@ -107,21 +124,6 @@ const EmailTemplatesOverview = ({ } }, [searchKey, emailTemplates]) - const templateCasesToFetch: string[] = [ - 'APPLICATION_CREATED_CHAIR', - 'THESIS_PRESENTATION_INVITATION_UPDATED', - 'THESIS_PRESENTATION_INVITATION', - 'THESIS_PRESENTATION_INVITATION_CANCELLED', - 'APPLICATION_REJECTED_TOPIC_REQUIREMENTS', - 'APPLICATION_REJECTED_TOPIC_OUTDATED', - 'APPLICATION_REJECTED', - 'APPLICATION_REJECTED_TITLE_NOT_INTERESTING', - 'APPLICATION_REJECTED_STUDENT_REQUIREMENTS', - 'APPLICATION_REJECTED_TOPIC_FILLED', - 'APPLICATION_ACCEPTED', - 'APPLICATION_ACCEPTED_NO_SUPERVISOR', - ] - const { researchGroupId } = useParams<{ researchGroupId: string }>() @@ -135,7 +137,7 @@ const EmailTemplatesOverview = ({ method: 'GET', requiresAuth: true, params: { - templateCases: templateCasesToFetch.join(','), + templateCases: TEMPLATE_CASES_TO_FETCH.join(','), page: 0, limit: -1, researchGroupId: researchGroupId, @@ -182,7 +184,7 @@ const EmailTemplatesOverview = ({ } }, ) - }, []) + }, [researchGroupId]) return ( <ResearchGroupSettingsCard title={'Email Templates'} diff --git a/client/src/pages/ResearchGroupSettingPage/components/EmailTemplateSettings/EmailTextEditor/EmailTextEditor.tsx b/client/src/pages/ResearchGroupSettingPage/components/EmailTemplateSettings/EmailTextEditor/EmailTextEditor.tsx index 399db0aa3..82581b45e 100644 --- a/client/src/pages/ResearchGroupSettingPage/components/EmailTemplateSettings/EmailTextEditor/EmailTextEditor.tsx +++ b/client/src/pages/ResearchGroupSettingPage/components/EmailTemplateSettings/EmailTextEditor/EmailTextEditor.tsx @@ -1,11 +1,15 @@ import { Link, RichTextEditor, useRichTextEditorContext } from '@mantine/tiptap' import TextAlign from '@tiptap/extension-text-align' import Underline from '@tiptap/extension-underline' -import { Editor, useEditor } from '@tiptap/react' +import type { Editor } from '@tiptap/react' +import { useEditor } from '@tiptap/react' import StarterKit from '@tiptap/starter-kit' import { Combobox, Group, useCombobox } from '@mantine/core' import ReactComponent from './Extension' -import { IEmailTemplate, IMailVariableDto } from '../../../../../requests/responses/emailtemplate' +import type { + IEmailTemplate, + IMailVariableDto, +} from '../../../../../requests/responses/emailtemplate' import { useEffect, useMemo, useState } from 'react' import { CaretDownIcon, CaretUpIcon, Plus } from '@phosphor-icons/react' import { FontSize, TextStyle } from '@tiptap/extension-text-style' @@ -86,7 +90,7 @@ const EmailTextEditor = ({ onUpdate: ({ editor: ed }) => { if (setEditingTemplate && editingTemplate) { setEditingTemplate({ - ...editingTemplate!, + ...editingTemplate, bodyHtml: convertHtmlToTemplateVariables(ed.getHTML(), templateVariables), }) } @@ -115,6 +119,7 @@ const EmailTextEditor = ({ convertTemplateVariablesToHtml(editingTemplate.bodyHtml ?? '', templateVariables), ) } + // eslint-disable-next-line @eslint-react/exhaustive-deps -- only re-sync editor content when bodyHtml or its variable mapping changes; full editingTemplate identity would loop }, [editingTemplate?.bodyHtml, editor, templateVariables]) const fetchTemplateVariables = async () => { @@ -138,8 +143,9 @@ const EmailTextEditor = ({ useEffect(() => { if (editingTemplate) { - fetchTemplateVariables() + void fetchTemplateVariables() } + // eslint-disable-next-line @eslint-react/exhaustive-deps -- fetchTemplateVariables is recreated each render; only re-run when the editingTemplate id changes }, [editingTemplate?.id]) return ( diff --git a/client/src/pages/ResearchGroupSettingPage/components/EmailTemplateSettings/EmailTextEditor/Extension.ts b/client/src/pages/ResearchGroupSettingPage/components/EmailTemplateSettings/EmailTextEditor/Extension.ts index 4d244febf..65b4efe23 100644 --- a/client/src/pages/ResearchGroupSettingPage/components/EmailTemplateSettings/EmailTextEditor/Extension.ts +++ b/client/src/pages/ResearchGroupSettingPage/components/EmailTemplateSettings/EmailTextEditor/Extension.ts @@ -1,7 +1,7 @@ import { Node } from '@tiptap/core' import { ReactNodeViewRenderer } from '@tiptap/react' import VariableComponent from './VariableComponent' -import { IMailVariableDto } from '../../../../../requests/responses/emailtemplate' +import type { IMailVariableDto } from '../../../../../requests/responses/emailtemplate' export default Node.create({ name: 'react-component', diff --git a/client/src/pages/ResearchGroupSettingPage/components/EmailTemplateSettings/EmailTextEditor/VariableComboboxOptions.tsx b/client/src/pages/ResearchGroupSettingPage/components/EmailTemplateSettings/EmailTextEditor/VariableComboboxOptions.tsx index ee3eb5bf9..0c36c00d0 100644 --- a/client/src/pages/ResearchGroupSettingPage/components/EmailTemplateSettings/EmailTextEditor/VariableComboboxOptions.tsx +++ b/client/src/pages/ResearchGroupSettingPage/components/EmailTemplateSettings/EmailTextEditor/VariableComboboxOptions.tsx @@ -1,6 +1,6 @@ import { Combobox } from '@mantine/core' import { useMemo } from 'react' -import { IMailVariableDto } from '../../../../../requests/responses/emailtemplate' +import type { IMailVariableDto } from '../../../../../requests/responses/emailtemplate' import { MagnifyingGlassIcon } from '@phosphor-icons/react' interface IVariableComboboxOptionsProps { diff --git a/client/src/pages/ResearchGroupSettingPage/components/EmailTemplateSettings/EmailTextEditor/VariableComponent.tsx b/client/src/pages/ResearchGroupSettingPage/components/EmailTemplateSettings/EmailTextEditor/VariableComponent.tsx index 36c3264a7..a3a71c5e1 100644 --- a/client/src/pages/ResearchGroupSettingPage/components/EmailTemplateSettings/EmailTextEditor/VariableComponent.tsx +++ b/client/src/pages/ResearchGroupSettingPage/components/EmailTemplateSettings/EmailTextEditor/VariableComponent.tsx @@ -1,8 +1,9 @@ import { Badge, Combobox, Group, useCombobox } from '@mantine/core' import { CaretDownIcon, CaretUpIcon } from '@phosphor-icons/react' -import { NodeViewWrapper, NodeViewProps } from '@tiptap/react' +import type { NodeViewProps } from '@tiptap/react' +import { NodeViewWrapper } from '@tiptap/react' import { useEffect, useState } from 'react' -import { IMailVariableDto } from '../../../../../requests/responses/emailtemplate' +import type { IMailVariableDto } from '../../../../../requests/responses/emailtemplate' import VariableComboboxOptions from './VariableComboboxOptions' export default function VariableComponent(props: NodeViewProps) { diff --git a/client/src/pages/ResearchGroupSettingPage/components/GeneralResearchGroupSettings.tsx b/client/src/pages/ResearchGroupSettingPage/components/GeneralResearchGroupSettings.tsx index 64cf48726..adac427bb 100644 --- a/client/src/pages/ResearchGroupSettingPage/components/GeneralResearchGroupSettings.tsx +++ b/client/src/pages/ResearchGroupSettingPage/components/GeneralResearchGroupSettings.tsx @@ -3,8 +3,8 @@ import { doRequest } from '../../../requests/request' import { showNotification } from '@mantine/notifications' import { showSimpleError } from '../../../utils/notification' import { getApiResponseErrorMessage } from '../../../requests/handler' -import { IResearchGroup } from '../../../requests/responses/researchGroup' -import { ResearchGroupFormValues } from '../../ResearchGroupAdminPage/components/CreateResearchGroupModal' +import type { IResearchGroup } from '../../../requests/responses/researchGroup' +import type { ResearchGroupFormValues } from '../../ResearchGroupAdminPage/components/CreateResearchGroupModal' import ResearchGroupForm from '../../../components/ResearchGroupForm/ResearchGroupForm' interface IGeneralResearchGroupSettingsProps { diff --git a/client/src/pages/ResearchGroupSettingPage/components/GradingSchemeSettingsCard.tsx b/client/src/pages/ResearchGroupSettingPage/components/GradingSchemeSettingsCard.tsx index 6c727d929..1e3f3f44a 100644 --- a/client/src/pages/ResearchGroupSettingPage/components/GradingSchemeSettingsCard.tsx +++ b/client/src/pages/ResearchGroupSettingPage/components/GradingSchemeSettingsCard.tsx @@ -15,7 +15,7 @@ import { useParams } from 'react-router' import { doRequest } from '../../../requests/request' import { showSimpleError } from '../../../utils/notification' import { getApiResponseErrorMessage } from '../../../requests/handler' -import { +import type { IGradingSchemeComponent, IResearchGroupSettings, IResearchGroupSettingsGradingScheme, diff --git a/client/src/pages/ResearchGroupSettingPage/components/MemberSettings/DeleteMemberModal.tsx b/client/src/pages/ResearchGroupSettingPage/components/MemberSettings/DeleteMemberModal.tsx index 0bfe9073b..8a51b3af4 100644 --- a/client/src/pages/ResearchGroupSettingPage/components/MemberSettings/DeleteMemberModal.tsx +++ b/client/src/pages/ResearchGroupSettingPage/components/MemberSettings/DeleteMemberModal.tsx @@ -1,5 +1,5 @@ import { Modal, Text, Button, Group, Alert, Stack } from '@mantine/core' -import { ILightUser } from '../../../../requests/responses/user' +import type { ILightUser } from '../../../../requests/responses/user' import { WarningCircleIcon } from '@phosphor-icons/react' type IDeleteMemberModalProps = { diff --git a/client/src/pages/ResearchGroupSettingPage/components/MemberSettings/ResearchGroupMembers.tsx b/client/src/pages/ResearchGroupSettingPage/components/MemberSettings/ResearchGroupMembers.tsx index ab8e5eab3..053c14c3f 100644 --- a/client/src/pages/ResearchGroupSettingPage/components/MemberSettings/ResearchGroupMembers.tsx +++ b/client/src/pages/ResearchGroupSettingPage/components/MemberSettings/ResearchGroupMembers.tsx @@ -21,7 +21,7 @@ import { } from '@phosphor-icons/react' import AddResearchGroupMemberModal from './AddResearchGroupMemberModal' import { useEffect, useState } from 'react' -import { ILightUser } from '../../../../requests/responses/user' +import type { ILightUser } from '../../../../requests/responses/user' import { doRequest } from '../../../../requests/request' import { showSimpleError } from '../../../../utils/notification' import { getApiResponseErrorMessage } from '../../../../requests/handler' @@ -29,7 +29,7 @@ import { showNotification } from '@mantine/notifications' import UserInformationRow from '../../../../components/UserInformationRow/UserInformationRow' import DeleteMemberModal from './DeleteMemberModal' import { ResearchGroupSettingsCard } from '../ResearchGroupSettingsCard' -import { IResearchGroup } from '../../../../requests/responses/researchGroup' +import type { IResearchGroup } from '../../../../requests/responses/researchGroup' interface IResearchGroupMembersProps { researchGroupData: IResearchGroup | undefined @@ -72,9 +72,10 @@ const ResearchGroupMembers = ({ researchGroupData }: IResearchGroupMembersProps) useEffect(() => { fetchMembers() + // eslint-disable-next-line @eslint-react/exhaustive-deps -- fetchMembers is recreated each render; only re-fetch when the research group changes }, [researchGroupData]) - const handleAddMember = async (username: string) => { + const handleAddMember = (username: string) => { if (!researchGroupData?.id) return doRequest<ILightUser>( @@ -99,7 +100,7 @@ const ResearchGroupMembers = ({ researchGroupData }: IResearchGroupMembersProps) ) } - const handleRemoveMember = async (userId: string) => { + const handleRemoveMember = (userId: string) => { if (!researchGroupData?.id) return doRequest<ILightUser>( @@ -124,7 +125,7 @@ const ResearchGroupMembers = ({ researchGroupData }: IResearchGroupMembersProps) ) } - const updateMemberRole = async (userId: string, newRole: string) => { + const updateMemberRole = (userId: string, newRole: string) => { if (!researchGroupData?.id) return doRequest<ILightUser>( @@ -159,7 +160,7 @@ const ResearchGroupMembers = ({ researchGroupData }: IResearchGroupMembersProps) ) } - const updateGroupAdminForUser = async (userId: string, userName: string) => { + const updateGroupAdminForUser = (userId: string, userName: string) => { if (!researchGroupData?.id) return doRequest<ILightUser>( diff --git a/client/src/pages/ResearchGroupSettingPage/components/PresentationSettingsCard.tsx b/client/src/pages/ResearchGroupSettingPage/components/PresentationSettingsCard.tsx index d73a734d2..95f1a68e2 100644 --- a/client/src/pages/ResearchGroupSettingPage/components/PresentationSettingsCard.tsx +++ b/client/src/pages/ResearchGroupSettingPage/components/PresentationSettingsCard.tsx @@ -4,7 +4,7 @@ import { doRequest } from '../../../requests/request' import { useParams } from 'react-router' import { showSimpleError } from '../../../utils/notification' import { getApiResponseErrorMessage } from '../../../requests/handler' -import { IResearchGroupSettings } from '../../../requests/responses/researchGroupSettings' +import type { IResearchGroupSettings } from '../../../requests/responses/researchGroupSettings' interface PresentaionSettingsProps { presentationDurationSettings: number diff --git a/client/src/pages/ResearchGroupSettingPage/components/ProposalSettingsCard.tsx b/client/src/pages/ResearchGroupSettingPage/components/ProposalSettingsCard.tsx index 7424ad112..74d151d1d 100644 --- a/client/src/pages/ResearchGroupSettingPage/components/ProposalSettingsCard.tsx +++ b/client/src/pages/ResearchGroupSettingPage/components/ProposalSettingsCard.tsx @@ -4,7 +4,7 @@ import { doRequest } from '../../../requests/request' import { useParams } from 'react-router' import { showSimpleError } from '../../../utils/notification' import { getApiResponseErrorMessage } from '../../../requests/handler' -import { IResearchGroupSettings } from '../../../requests/responses/researchGroupSettings' +import type { IResearchGroupSettings } from '../../../requests/responses/researchGroupSettings' interface ProposalSettingsCardProps { proposalPhaseActive: boolean diff --git a/client/src/pages/ResearchGroupSettingPage/components/ResearchGroupSettingsCard.tsx b/client/src/pages/ResearchGroupSettingPage/components/ResearchGroupSettingsCard.tsx index 1f8881fdd..8f6c2f174 100644 --- a/client/src/pages/ResearchGroupSettingPage/components/ResearchGroupSettingsCard.tsx +++ b/client/src/pages/ResearchGroupSettingPage/components/ResearchGroupSettingsCard.tsx @@ -1,5 +1,5 @@ import { Card, Stack, Title, Text } from '@mantine/core' -import { ReactNode } from 'react' +import type { ReactNode } from 'react' interface IResearchGroupSettingsCardProps { title: string diff --git a/client/src/pages/ResearchGroupSettingPage/components/ScientificWritingGuideSettingsCard.tsx b/client/src/pages/ResearchGroupSettingPage/components/ScientificWritingGuideSettingsCard.tsx index ababc167f..dd8b0b951 100644 --- a/client/src/pages/ResearchGroupSettingPage/components/ScientificWritingGuideSettingsCard.tsx +++ b/client/src/pages/ResearchGroupSettingPage/components/ScientificWritingGuideSettingsCard.tsx @@ -7,7 +7,7 @@ import { getApiResponseErrorMessage } from '../../../requests/handler' import { showSimpleError } from '../../../utils/notification' import { showNotification } from '@mantine/notifications' import { useParams } from 'react-router' -import { +import type { IResearchGroupSettings, IResearchGroupSettingsWritingGuide, } from '../../../requests/responses/researchGroupSettings' @@ -47,6 +47,7 @@ const ScientificWritingGuideSettingsCard = ({ form.setValues({ scientificWritingGuideLink: writingGuideSettings?.scientificWritingGuideLink ?? '', }) + // eslint-disable-next-line @eslint-react/exhaustive-deps -- form is stable; only re-sync when the server-provided link value changes }, [writingGuideSettings?.scientificWritingGuideLink]) const hasChanges = diff --git a/client/src/pages/ReviewApplicationPage/ReviewApplicationPage.tsx b/client/src/pages/ReviewApplicationPage/ReviewApplicationPage.tsx index 02b565334..f25c7bbda 100644 --- a/client/src/pages/ReviewApplicationPage/ReviewApplicationPage.tsx +++ b/client/src/pages/ReviewApplicationPage/ReviewApplicationPage.tsx @@ -1,6 +1,7 @@ import React, { useEffect, useState } from 'react' import { useNavigate, useParams } from 'react-router' -import { ApplicationState, IApplication } from '../../requests/responses/application' +import type { IApplication } from '../../requests/responses/application' +import { ApplicationState } from '../../requests/responses/application' import { doRequest } from '../../requests/request' import ApplicationsProvider from '../../providers/ApplicationsProvider/ApplicationsProvider' import { Grid, Text, Center } from '@mantine/core' @@ -47,7 +48,7 @@ const ReviewApplicationPage = () => { <ApplicationModal application={application} onClose={() => { - navigate('/applications', { replace: true }) + void navigate('/applications', { replace: true }) setApplication(undefined) }} @@ -63,7 +64,7 @@ const ReviewApplicationPage = () => { selected={application} isSmallScreen={isSmallScreen} onSelect={(newApplication) => { - navigate(`/applications/${newApplication.applicationId}`, { + void navigate(`/applications/${newApplication.applicationId}`, { replace: true, }) @@ -79,7 +80,7 @@ const ReviewApplicationPage = () => { onChange={setApplication} onDelete={() => { setApplication(undefined) - navigate('/applications', { replace: true }) + void navigate('/applications', { replace: true }) }} /> ) : ( diff --git a/client/src/pages/ReviewApplicationPage/components/ApplicationListItem/ApplicationListItem.tsx b/client/src/pages/ReviewApplicationPage/components/ApplicationListItem/ApplicationListItem.tsx index f89561e5b..6a833ddae 100644 --- a/client/src/pages/ReviewApplicationPage/components/ApplicationListItem/ApplicationListItem.tsx +++ b/client/src/pages/ReviewApplicationPage/components/ApplicationListItem/ApplicationListItem.tsx @@ -1,5 +1,5 @@ import { Badge, Group, Stack, Text, Paper } from '@mantine/core' -import { IApplication } from '../../../../requests/responses/application' +import type { IApplication } from '../../../../requests/responses/application' import { ApplicationStateColor } from '../../../../config/colors' import { formatApplicationState, formatDate } from '../../../../utils/format' import React from 'react' diff --git a/client/src/pages/ReviewApplicationPage/components/ApplicationReviewBody/ApplicationReviewBody.tsx b/client/src/pages/ReviewApplicationPage/components/ApplicationReviewBody/ApplicationReviewBody.tsx index 1167484cf..d59952c5e 100644 --- a/client/src/pages/ReviewApplicationPage/components/ApplicationReviewBody/ApplicationReviewBody.tsx +++ b/client/src/pages/ReviewApplicationPage/components/ApplicationReviewBody/ApplicationReviewBody.tsx @@ -2,7 +2,7 @@ import ApplicationData from '../../../../components/ApplicationData/ApplicationD import ApplicationReviewForm from '../../../../components/ApplicationReviewForm/ApplicationReviewForm' import { Divider, Group, Stack } from '@mantine/core' import React, { useEffect } from 'react' -import { IApplication } from '../../../../requests/responses/application' +import type { IApplication } from '../../../../requests/responses/application' import ApplicationRejectButton from '../../../../components/ApplicationRejectButton/ApplicationRejectButton' import ApplicationDeleteButton from '../../../../components/ApplicationDeleteButton/ApplicationDeleteButton' diff --git a/client/src/pages/ReviewApplicationPage/components/ApplicationsSidebar/ApplicationsSidebar.tsx b/client/src/pages/ReviewApplicationPage/components/ApplicationsSidebar/ApplicationsSidebar.tsx index 4940c840c..17bdf443d 100644 --- a/client/src/pages/ReviewApplicationPage/components/ApplicationsSidebar/ApplicationsSidebar.tsx +++ b/client/src/pages/ReviewApplicationPage/components/ApplicationsSidebar/ApplicationsSidebar.tsx @@ -2,7 +2,7 @@ import { Center, Pagination, Stack, Text } from '@mantine/core' import ApplicationsFilters from '../../../../components/ApplicationsFilters/ApplicationsFilters' import React, { useEffect, useRef, useState } from 'react' import { shouldIgnoreArrowKey } from './keyNavigationFilter' -import { IApplication } from '../../../../requests/responses/application' +import type { IApplication } from '../../../../requests/responses/application' import { useApplicationsContext } from '../../../../providers/ApplicationsProvider/hooks' import ApplicationListItem from '../ApplicationListItem/ApplicationListItem' @@ -84,6 +84,8 @@ const ApplicationsSidebar = (props: IApplicationsSidebarProps) => { } }, []) + const applicationIdsKey = (applications?.content ?? []).map((x) => x.applicationId).join(',') + useEffect(() => { if (isSmallScreen) { return @@ -100,12 +102,8 @@ const ApplicationsSidebar = (props: IApplicationsSidebarProps) => { : (applications.content ?? [])[0], ) } - }, [ - page, - startAtLastApplication, - isSmallScreen, - (applications?.content ?? []).map((x) => x.applicationId).join(','), - ]) + // eslint-disable-next-line @eslint-react/exhaustive-deps -- onSelect is recreated by the parent each render; applications identity is tracked via applicationIdsKey + }, [page, startAtLastApplication, isSmallScreen, applicationIdsKey]) return ( <Stack gap='sm'> @@ -129,7 +127,7 @@ const ApplicationsSidebar = (props: IApplicationsSidebarProps) => { <Center> <Pagination size='sm' - total={applications?.totalPages || 0} + total={applications?.totalPages ?? 0} value={page + 1} onChange={(newPage) => setPage(newPage - 1)} /> diff --git a/client/src/pages/ReviewApplicationPage/components/ApplicationsSidebar/keyNavigationFilter.ts b/client/src/pages/ReviewApplicationPage/components/ApplicationsSidebar/keyNavigationFilter.ts index 0218ee6d8..9173acc70 100644 --- a/client/src/pages/ReviewApplicationPage/components/ApplicationsSidebar/keyNavigationFilter.ts +++ b/client/src/pages/ReviewApplicationPage/components/ApplicationsSidebar/keyNavigationFilter.ts @@ -18,7 +18,7 @@ export const shouldIgnoreArrowKey = (event: KeyboardEvent): boolean => { target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || target instanceof HTMLSelectElement || - (target && target.isContentEditable) + target?.isContentEditable ) { return true } diff --git a/client/src/pages/SettingsPage/SettingsPage.tsx b/client/src/pages/SettingsPage/SettingsPage.tsx index a664088cb..2795e81be 100644 --- a/client/src/pages/SettingsPage/SettingsPage.tsx +++ b/client/src/pages/SettingsPage/SettingsPage.tsx @@ -12,10 +12,10 @@ const SettingsPage = () => { const navigate = useNavigate() - const value = tab || 'my-information' + const value = tab ?? 'my-information' return ( - <Tabs value={value} onChange={(newValue) => navigate(`/settings/${newValue}`)}> + <Tabs value={value} onChange={(newValue) => void navigate(`/settings/${newValue}`)}> <Tabs.List> <Tabs.Tab value='my-information' leftSection={<User />}> My Information diff --git a/client/src/pages/SettingsPage/components/AccountDeletion/AccountDeletion.tsx b/client/src/pages/SettingsPage/components/AccountDeletion/AccountDeletion.tsx index 389510afe..15851046d 100644 --- a/client/src/pages/SettingsPage/components/AccountDeletion/AccountDeletion.tsx +++ b/client/src/pages/SettingsPage/components/AccountDeletion/AccountDeletion.tsx @@ -46,7 +46,7 @@ const AccountDeletion = () => { setLoading(false) } } - fetchPreview() + void fetchPreview() }, []) const onDelete = async () => { @@ -59,7 +59,7 @@ const AccountDeletion = () => { }) if (response.ok) { showSimpleSuccess(response.data.message) - navigate('/logout') + void navigate('/logout') } else { showSimpleError(getApiResponseErrorMessage(response)) } @@ -150,7 +150,7 @@ const AccountDeletion = () => { > Cancel </Button> - <Button color='red' disabled={confirmName !== fullName} onClick={onDelete}> + <Button color='red' disabled={confirmName !== fullName} onClick={() => void onDelete()}> Yes, Delete My Account </Button> </Group> diff --git a/client/src/pages/SettingsPage/components/DataExport/DataExport.tsx b/client/src/pages/SettingsPage/components/DataExport/DataExport.tsx index d2682fef4..d33959b83 100644 --- a/client/src/pages/SettingsPage/components/DataExport/DataExport.tsx +++ b/client/src/pages/SettingsPage/components/DataExport/DataExport.tsx @@ -39,7 +39,7 @@ const DataExport = () => { } useEffect(() => { - fetchStatus() + void fetchStatus() }, []) const onRequest = async () => { @@ -162,12 +162,12 @@ const DataExport = () => { <Group> {isDownloadable && ( - <Button onClick={onDownload} loading={downloading}> + <Button onClick={() => void onDownload()} loading={downloading}> Download Export </Button> )} <Button - onClick={onRequest} + onClick={() => void onRequest()} loading={requesting} disabled={loading || !status?.canRequest || isProcessing} variant={isDownloadable ? 'light' : 'filled'} diff --git a/client/src/pages/SettingsPage/components/NotificationSettings/components/NotificationSelect/NotificationSelect.tsx b/client/src/pages/SettingsPage/components/NotificationSettings/components/NotificationSelect/NotificationSelect.tsx index 9c9edf8ad..1588bbd49 100644 --- a/client/src/pages/SettingsPage/components/NotificationSettings/components/NotificationSelect/NotificationSelect.tsx +++ b/client/src/pages/SettingsPage/components/NotificationSettings/components/NotificationSelect/NotificationSelect.tsx @@ -28,7 +28,7 @@ export function NotificationSelect({ const handleChange = (value: string | null) => { if (value) { - updateSetting(value) + void updateSetting(value) } } diff --git a/client/src/pages/SettingsPage/components/NotificationSettings/components/NotificationToggleSwitch/NotificationToggleSwitch.tsx b/client/src/pages/SettingsPage/components/NotificationSettings/components/NotificationToggleSwitch/NotificationToggleSwitch.tsx index 3790e3600..8c0cf80a6 100644 --- a/client/src/pages/SettingsPage/components/NotificationSettings/components/NotificationToggleSwitch/NotificationToggleSwitch.tsx +++ b/client/src/pages/SettingsPage/components/NotificationSettings/components/NotificationToggleSwitch/NotificationToggleSwitch.tsx @@ -1,4 +1,5 @@ -import React, { Dispatch, SetStateAction } from 'react' +import type { Dispatch, SetStateAction } from 'react' +import React from 'react' import { Switch, type BoxProps } from '@mantine/core' import { useNotificationSetting } from '../../../../../../hooks/notification' @@ -19,8 +20,8 @@ export const NotificationToggleSwitch = (props: INotificationToggleSwitchProps) const isChecked = currentEmail !== 'none' - const toggleSetting = async () => { - updateSetting(isChecked ? 'none' : 'all') + const toggleSetting = () => { + void updateSetting(isChecked ? 'none' : 'all') } return <Switch checked={isChecked} onChange={toggleSetting} disabled={loading} {...other} /> diff --git a/client/src/pages/ThesisPage/components/FileHistoryTable/FileHistoryTable.tsx b/client/src/pages/ThesisPage/components/FileHistoryTable/FileHistoryTable.tsx index d08db6626..df0c18c3a 100644 --- a/client/src/pages/ThesisPage/components/FileHistoryTable/FileHistoryTable.tsx +++ b/client/src/pages/ThesisPage/components/FileHistoryTable/FileHistoryTable.tsx @@ -1,5 +1,5 @@ -import { ILightUser } from '../../../../requests/responses/user' -import { UploadFileType } from '../../../../config/types' +import type { ILightUser } from '../../../../requests/responses/user' +import type { UploadFileType } from '../../../../config/types' import { Button, Center, Group, Input, Table, Text } from '@mantine/core' import { formatDate } from '../../../../utils/format' import { AuthenticatedFilePreviewButton } from '../../../../components/AuthenticatedFilePreviewButton/AuthenticatedFilePreviewButton' @@ -82,7 +82,7 @@ const FileHistoryTable = (props: IFileHistoryTableProps) => { <DownloadSimple /> </AuthenticatedFileDownloadButton> {row.onDelete && ( - <Button loading={loading} size='xs' onClick={() => onDelete(row)}> + <Button loading={loading} size='xs' onClick={() => void onDelete(row)}> <Trash /> </Button> )} diff --git a/client/src/pages/ThesisPage/components/ThesisAssessmentSection/components/ReplaceAssessmentModal/ReplaceAssessmentModal.tsx b/client/src/pages/ThesisPage/components/ThesisAssessmentSection/components/ReplaceAssessmentModal/ReplaceAssessmentModal.tsx index 4c659197a..b13b39ea7 100644 --- a/client/src/pages/ThesisPage/components/ThesisAssessmentSection/components/ReplaceAssessmentModal/ReplaceAssessmentModal.tsx +++ b/client/src/pages/ThesisPage/components/ThesisAssessmentSection/components/ReplaceAssessmentModal/ReplaceAssessmentModal.tsx @@ -15,13 +15,13 @@ import { Plus, Trash } from '@phosphor-icons/react' import { useEffect, useState } from 'react' import DocumentEditor from '../../../../../../components/DocumentEditor/DocumentEditor' import { doRequest } from '../../../../../../requests/request' -import { IThesis } from '../../../../../../requests/responses/thesis' +import type { IThesis } from '../../../../../../requests/responses/thesis' import { useLoadedThesisContext, useThesisUpdateAction, } from '../../../../../../providers/ThesisProvider/hooks' import { ApiError } from '../../../../../../requests/handler' -import { IResearchGroupSettingsGradingScheme } from '../../../../../../requests/responses/researchGroupSettings' +import type { IResearchGroupSettingsGradingScheme } from '../../../../../../requests/responses/researchGroupSettings' import { calculateGradeFromComponents } from '../../../../../../utils/grade' interface IGradeComponent { @@ -131,10 +131,10 @@ const ReplaceAssessmentModal = (props: IReplaceAssessmentModalProps) => { }, 'Assessment submitted successfully') useEffect(() => { - setSummary(thesis.assessment?.summary || '') - setPositives(thesis.assessment?.positives || '') - setNegatives(thesis.assessment?.negatives || '') - setGradeSuggestion(thesis.assessment?.gradeSuggestion || '') + setSummary(thesis.assessment?.summary ?? '') + setPositives(thesis.assessment?.positives ?? '') + setNegatives(thesis.assessment?.negatives ?? '') + setGradeSuggestion(thesis.assessment?.gradeSuggestion ?? '') setGradeSuggestionManuallyEdited(false) diff --git a/client/src/pages/ThesisPage/components/ThesisConfigSection/ThesisConfigSection.tsx b/client/src/pages/ThesisPage/components/ThesisConfigSection/ThesisConfigSection.tsx index c9d4dbbe1..b5532e039 100644 --- a/client/src/pages/ThesisPage/components/ThesisConfigSection/ThesisConfigSection.tsx +++ b/client/src/pages/ThesisPage/components/ThesisConfigSection/ThesisConfigSection.tsx @@ -1,4 +1,4 @@ -import { IThesis, ThesisState } from '../../../../requests/responses/thesis' +import type { IThesis, ThesisState } from '../../../../requests/responses/thesis' import { Accordion, Alert, @@ -31,8 +31,8 @@ import ThesisStateBadge from '../../../../components/ThesisStateBadge/ThesisStat import ThesisVisibilitySelect from '../ThesisVisibilitySelect/ThesisVisibilitySelect' import { formatThesisType } from '../../../../utils/format' import LanguageSelect from '../../../../components/LanguageSelect/LanguageSelect' -import { PaginationResponse } from '../../../../requests/responses/pagination' -import { ILightResearchGroup } from '../../../../requests/responses/researchGroup' +import type { PaginationResponse } from '../../../../requests/responses/pagination' +import type { ILightResearchGroup } from '../../../../requests/responses/researchGroup' import { showSimpleError, showSimpleSuccess } from '../../../../utils/notification' import { useHasGroupAccess } from '../../../../hooks/authentication' import { Warning } from '@phosphor-icons/react' @@ -137,6 +137,7 @@ const ThesisConfigSection = () => { useEffect(() => { form.validate() + // eslint-disable-next-line @eslint-react/exhaustive-deps -- form is stable; only re-validate when the relevant fields change }, [form.values.startDate, form.values.endDate, form.values.states]) useEffect(() => { @@ -159,6 +160,7 @@ const ThesisConfigSection = () => { }) form.reset() + // eslint-disable-next-line @eslint-react/exhaustive-deps -- form is stable; only re-seed when the thesis prop changes }, [thesis]) useEffect(() => { @@ -209,6 +211,7 @@ const ThesisConfigSection = () => { } }, ) + // eslint-disable-next-line @eslint-react/exhaustive-deps -- mount-only fetch of research groups; form/admin flag/thesis researchGroup are read but should not retrigger the request }, []) const [closing, onClose] = useThesisUpdateAction(async () => { @@ -255,7 +258,7 @@ const ThesisConfigSection = () => { if (response.ok) { showSimpleSuccess('Thesis anonymized successfully') setAnonymizeModalOpen(false) - navigate('/theses') + void navigate('/theses') } else { showSimpleError(getApiResponseErrorMessage(response)) } @@ -438,7 +441,7 @@ const ThesisConfigSection = () => { variant='outline' color='red' loading={anonymizeLoading} - onClick={onDeleteThesisClick} + onClick={() => void onDeleteThesisClick()} > Anonymize Thesis </Button> @@ -464,8 +467,8 @@ const ThesisConfigSection = () => { {anonymizeWarnings.length > 0 && ( <Alert color='orange' icon={<Warning />} title='Warnings'> <List size='sm'> - {anonymizeWarnings.map((warning, index) => ( - <List.Item key={index}>{warning}</List.Item> + {anonymizeWarnings.map((warning) => ( + <List.Item key={warning}>{warning}</List.Item> ))} </List> </Alert> @@ -479,7 +482,11 @@ const ThesisConfigSection = () => { <Button variant='default' onClick={() => setAnonymizeModalOpen(false)}> Cancel </Button> - <Button color='red' loading={anonymizeLoading} onClick={onConfirmAnonymize}> + <Button + color='red' + loading={anonymizeLoading} + onClick={() => void onConfirmAnonymize()} + > Anonymize Thesis </Button> </Group> diff --git a/client/src/pages/ThesisPage/components/ThesisFeedbackOverview/ThesisFeedbackOverview.tsx b/client/src/pages/ThesisPage/components/ThesisFeedbackOverview/ThesisFeedbackOverview.tsx index a161e29c2..79d52bb70 100644 --- a/client/src/pages/ThesisPage/components/ThesisFeedbackOverview/ThesisFeedbackOverview.tsx +++ b/client/src/pages/ThesisPage/components/ThesisFeedbackOverview/ThesisFeedbackOverview.tsx @@ -3,7 +3,7 @@ import { useThesisUpdateAction, } from '../../../../providers/ThesisProvider/hooks' import { Center, Checkbox, Input, Table, Text } from '@mantine/core' -import { IThesis } from '../../../../requests/responses/thesis' +import type { IThesis } from '../../../../requests/responses/thesis' import React from 'react' import AvatarUser from '../../../../components/AvatarUser/AvatarUser' import { formatDate } from '../../../../utils/format' @@ -86,7 +86,7 @@ const ThesisFeedbackOverview = (props: IThesisFeedbackOverviewProps) => { <Table.Tr key={item.feedbackId}> <Table.Td ta='center' width={50}> <Checkbox - checked={!!item.completedAt} + checked={Boolean(item.completedAt)} disabled={loading || !access.student || !allowEdit} onChange={() => toggleFeedback(item)} /> diff --git a/client/src/pages/ThesisPage/components/ThesisFeedbackRequestButton/ThesisFeedbackRequestButton.tsx b/client/src/pages/ThesisPage/components/ThesisFeedbackRequestButton/ThesisFeedbackRequestButton.tsx index 19827a27a..1da32f73a 100644 --- a/client/src/pages/ThesisPage/components/ThesisFeedbackRequestButton/ThesisFeedbackRequestButton.tsx +++ b/client/src/pages/ThesisPage/components/ThesisFeedbackRequestButton/ThesisFeedbackRequestButton.tsx @@ -5,7 +5,7 @@ import { } from '../../../../providers/ThesisProvider/hooks' import { Button, Checkbox, Modal, Stack, Textarea, Text, Title, Group } from '@mantine/core' import { doRequest } from '../../../../requests/request' -import { IThesis } from '../../../../requests/responses/thesis' +import type { IThesis } from '../../../../requests/responses/thesis' import { ApiError } from '../../../../requests/handler' interface IThesisFeedbackRequestButtonProps { @@ -132,10 +132,10 @@ const ThesisFeedbackRequestButton = (props: IThesisFeedbackRequestButtonProps) = label={change.feedback} checked={ editChanges.find((item) => item.feedbackId === change.feedbackId)?.completed ?? - !!change.completedAt + Boolean(change.completedAt) } onChange={(e) => { - if (e.target.checked === !!change.completedAt) { + if (e.target.checked === Boolean(change.completedAt)) { setEditChanges((prev) => prev.filter((item) => item.feedbackId !== change.feedbackId), ) diff --git a/client/src/pages/ThesisPage/components/ThesisFinalGradeSection/ThesisFinalGradeSection.tsx b/client/src/pages/ThesisPage/components/ThesisFinalGradeSection/ThesisFinalGradeSection.tsx index f8a8f5697..275e87c42 100644 --- a/client/src/pages/ThesisPage/components/ThesisFinalGradeSection/ThesisFinalGradeSection.tsx +++ b/client/src/pages/ThesisPage/components/ThesisFinalGradeSection/ThesisFinalGradeSection.tsx @@ -1,4 +1,5 @@ -import { IThesis, ThesisState } from '../../../../requests/responses/thesis' +import type { IThesis } from '../../../../requests/responses/thesis' +import { ThesisState } from '../../../../requests/responses/thesis' import { useState } from 'react' import { Accordion, Button, Group, Stack, Text } from '@mantine/core' import SubmitFinalGradeModal from './components/SubmitFinalGradeModal/SubmitFinalGradeModal' diff --git a/client/src/pages/ThesisPage/components/ThesisFinalGradeSection/components/SubmitFinalGradeModal/SubmitFinalGradeModal.tsx b/client/src/pages/ThesisPage/components/ThesisFinalGradeSection/components/SubmitFinalGradeModal/SubmitFinalGradeModal.tsx index e38aef3ec..340d33723 100644 --- a/client/src/pages/ThesisPage/components/ThesisFinalGradeSection/components/SubmitFinalGradeModal/SubmitFinalGradeModal.tsx +++ b/client/src/pages/ThesisPage/components/ThesisFinalGradeSection/components/SubmitFinalGradeModal/SubmitFinalGradeModal.tsx @@ -1,4 +1,4 @@ -import { IThesis } from '../../../../../../requests/responses/thesis' +import type { IThesis } from '../../../../../../requests/responses/thesis' import { Alert, Button, Modal, Stack, Text, TextInput } from '@mantine/core' import { doRequest } from '../../../../../../requests/request' import { useEffect, useState } from 'react' @@ -26,8 +26,8 @@ const SubmitFinalGradeModal = (props: ISubmitFinalGradeModalProps) => { const [visibility, setVisibility] = useState(thesis.visibility) useEffect(() => { - setFinalGrade(thesis.grade?.finalGrade || '') - setFeedback(thesis.grade?.feedback || '') + setFinalGrade(thesis.grade?.finalGrade ?? '') + setFeedback(thesis.grade?.feedback ?? '') setVisibility(thesis.visibility) }, [thesis]) diff --git a/client/src/pages/ThesisPage/components/ThesisInfoSection/ThesisInfoSection.tsx b/client/src/pages/ThesisPage/components/ThesisInfoSection/ThesisInfoSection.tsx index 042af5fd5..161d79a01 100644 --- a/client/src/pages/ThesisPage/components/ThesisInfoSection/ThesisInfoSection.tsx +++ b/client/src/pages/ThesisPage/components/ThesisInfoSection/ThesisInfoSection.tsx @@ -1,4 +1,4 @@ -import { IThesis } from '../../../../requests/responses/thesis' +import type { IThesis } from '../../../../requests/responses/thesis' import React, { useEffect, useState } from 'react' import { Accordion, Button, Flex, Grid, Group, Stack, TextInput } from '@mantine/core' import DocumentEditor from '../../../../components/DocumentEditor/DocumentEditor' diff --git a/client/src/pages/ThesisPage/components/ThesisInfoSection/components/DownloadAllFilesButton/DownloadAllFilesButton.tsx b/client/src/pages/ThesisPage/components/ThesisInfoSection/components/DownloadAllFilesButton/DownloadAllFilesButton.tsx index e402692d5..84ef1300f 100644 --- a/client/src/pages/ThesisPage/components/ThesisInfoSection/components/DownloadAllFilesButton/DownloadAllFilesButton.tsx +++ b/client/src/pages/ThesisPage/components/ThesisInfoSection/components/DownloadAllFilesButton/DownloadAllFilesButton.tsx @@ -8,8 +8,8 @@ import { showSimpleError } from '../../../../../../utils/notification' import { getApiResponseErrorMessage } from '../../../../../../requests/handler' import JSZip from 'jszip' import { downloadFile } from '../../../../../../utils/blob' -import { IThesisComment } from '../../../../../../requests/responses/thesis' -import { PaginationResponse } from '../../../../../../requests/responses/pagination' +import type { IThesisComment } from '../../../../../../requests/responses/thesis' +import type { PaginationResponse } from '../../../../../../requests/responses/pagination' const DownloadAllFilesButton = () => { const { thesis, access } = useLoadedThesisContext() @@ -133,7 +133,7 @@ const DownloadAllFilesButton = () => { } return ( - <Button variant='outline' onClick={onDownload} loading={loading}> + <Button variant='outline' onClick={() => void onDownload()} loading={loading}> Download All Files </Button> ) diff --git a/client/src/pages/ThesisPage/components/ThesisPresentationSection/ThesisPresentationSection.tsx b/client/src/pages/ThesisPage/components/ThesisPresentationSection/ThesisPresentationSection.tsx index ff3f416a1..78d7b7fda 100644 --- a/client/src/pages/ThesisPage/components/ThesisPresentationSection/ThesisPresentationSection.tsx +++ b/client/src/pages/ThesisPage/components/ThesisPresentationSection/ThesisPresentationSection.tsx @@ -32,9 +32,9 @@ const ThesisPresentationSection = () => { </Button> )} - {(thesis.presentations ?? []).map((presentation, index) => ( + {(thesis.presentations ?? []).map((presentation) => ( <PresentationCard - key={`presentation-${index}`} + key={presentation.presentationId} presentation={presentation} thesis={thesis} thesisType={thesis.type} diff --git a/client/src/pages/ThesisPage/components/ThesisPresentationSection/components/PresentationCard.tsx b/client/src/pages/ThesisPage/components/ThesisPresentationSection/components/PresentationCard.tsx index 9bf671b06..ea8d5f497 100644 --- a/client/src/pages/ThesisPage/components/ThesisPresentationSection/components/PresentationCard.tsx +++ b/client/src/pages/ThesisPage/components/ThesisPresentationSection/components/PresentationCard.tsx @@ -13,7 +13,7 @@ import { Alert, Transition, } from '@mantine/core' -import { +import type { IPublishedPresentation, IPublishedThesis, IThesis, @@ -226,7 +226,7 @@ const PresentationCard = ({ <Group justify='space-between' align={'flex-start'} gap={'0.5rem'} wrap='nowrap'> <Stack gap={'0.5rem'}> <Title order={titleOrder ?? 5}> - {thesisName ? thesisName : `${formatThesisType(thesisType)} Presentation`} + {thesisName ?? `${formatThesisType(thesisType)} Presentation`} { onChange={(value) => setCredits((prev) => { if (value) { - return { ...prev, [user.data.userId]: +value } + return { ...prev, [user.data.userId]: Number(value) } } else { delete prev[user.data.userId] diff --git a/client/src/pages/ThesisPage/components/ThesisVisibilitySelect/ThesisVisibilitySelect.tsx b/client/src/pages/ThesisPage/components/ThesisVisibilitySelect/ThesisVisibilitySelect.tsx index 9a6d74c1f..3f4f888d8 100644 --- a/client/src/pages/ThesisPage/components/ThesisVisibilitySelect/ThesisVisibilitySelect.tsx +++ b/client/src/pages/ThesisPage/components/ThesisVisibilitySelect/ThesisVisibilitySelect.tsx @@ -1,4 +1,5 @@ -import { Select, SelectProps, Text } from '@mantine/core' +import type { SelectProps } from '@mantine/core' +import { Select, Text } from '@mantine/core' const ThesisVisibilitySelect = (props: SelectProps) => { const { ...others } = props diff --git a/client/src/pages/ThesisPage/components/ThesisWritingSection/ThesisWritingSection.tsx b/client/src/pages/ThesisPage/components/ThesisWritingSection/ThesisWritingSection.tsx index 2134b6048..ad86fdc34 100644 --- a/client/src/pages/ThesisPage/components/ThesisWritingSection/ThesisWritingSection.tsx +++ b/client/src/pages/ThesisPage/components/ThesisWritingSection/ThesisWritingSection.tsx @@ -1,4 +1,5 @@ -import { IThesis, ThesisState } from '../../../../requests/responses/thesis' +import type { IThesis } from '../../../../requests/responses/thesis' +import { ThesisState } from '../../../../requests/responses/thesis' import { Accordion, Center, Grid, Group, Stack, Text, Table, Alert } from '@mantine/core' import ConfirmationButton from '../../../../components/ConfirmationButton/ConfirmationButton' import { doRequest } from '../../../../requests/request' @@ -90,7 +91,7 @@ const ThesisWritingSection = () => { ]), ) const requiredFilesUploaded = - !!thesisFile && + Boolean(thesisFile) && !Object.entries(GLOBAL_CONFIG.thesis_files) .filter(([, value]) => value.required) .some(([key]) => !customFiles[key]) diff --git a/client/src/pages/TopicPage/components/TopicAdittionalInformationCard.tsx b/client/src/pages/TopicPage/components/TopicAdittionalInformationCard.tsx index a9e82fa94..4b0398a5e 100644 --- a/client/src/pages/TopicPage/components/TopicAdittionalInformationCard.tsx +++ b/client/src/pages/TopicPage/components/TopicAdittionalInformationCard.tsx @@ -1,5 +1,5 @@ import { Card, Stack, Text, Divider, useMantineColorScheme } from '@mantine/core' -import { ITopic } from '../../../requests/responses/topic' +import type { ITopic } from '../../../requests/responses/topic' import { Buildings, Clock, GraduationCapIcon, Users } from '@phosphor-icons/react' import TopicAdittionalInformationSection from './TopicAdditionalInformationSection' import ThesisTypeBadge from '../../LandingPage/components/ThesisTypBadge/ThesisTypBadge' diff --git a/client/src/providers/ApplicationsProvider/ApplicationsProvider.tsx b/client/src/providers/ApplicationsProvider/ApplicationsProvider.tsx index 959ae232a..b2741bc7f 100644 --- a/client/src/providers/ApplicationsProvider/ApplicationsProvider.tsx +++ b/client/src/providers/ApplicationsProvider/ApplicationsProvider.tsx @@ -1,13 +1,10 @@ -import React, { PropsWithChildren, ReactNode, useEffect, useMemo, useRef, useState } from 'react' +import type { PropsWithChildren, ReactNode } from 'react' +import React, { useEffect, useMemo, useRef, useState } from 'react' import { doRequest } from '../../requests/request' -import { PaginationResponse } from '../../requests/responses/pagination' -import { - ApplicationsContext, - IApplicationsContext, - IApplicationsFilters, - IApplicationsSort, -} from './context' -import { ApplicationState, IApplication } from '../../requests/responses/application' +import type { PaginationResponse } from '../../requests/responses/pagination' +import type { IApplicationsContext, IApplicationsFilters, IApplicationsSort } from './context' +import { ApplicationsContext } from './context' +import type { ApplicationState, IApplication } from '../../requests/responses/application' import { useDebouncedValue } from '@mantine/hooks' import { showSimpleError } from '../../utils/notification' import { getApiResponseErrorMessage } from '../../requests/handler' @@ -73,7 +70,12 @@ const ApplicationsProvider = (props: PropsWithChildren { setPage(0) @@ -133,17 +135,18 @@ const ApplicationsProvider = (props: PropsWithChildren => { @@ -205,15 +208,13 @@ const ApplicationsProvider = (props: PropsWithChildren{emptyComponent} } - return ( - {children} - ) + return {children} } export default ApplicationsProvider diff --git a/client/src/providers/ApplicationsProvider/context.ts b/client/src/providers/ApplicationsProvider/context.ts index a517f7be0..fdc5b5aa7 100644 --- a/client/src/providers/ApplicationsProvider/context.ts +++ b/client/src/providers/ApplicationsProvider/context.ts @@ -1,7 +1,8 @@ -import React, { Dispatch, SetStateAction } from 'react' -import { PaginationResponse } from '../../requests/responses/pagination' -import { ApplicationState, IApplication } from '../../requests/responses/application' -import { ITopicOverview } from '../../requests/responses/topic' +import type { Dispatch, SetStateAction } from 'react' +import React from 'react' +import type { PaginationResponse } from '../../requests/responses/pagination' +import type { ApplicationState, IApplication } from '../../requests/responses/application' +import type { ITopicOverview } from '../../requests/responses/topic' export interface IApplicationsFilters { search?: string diff --git a/client/src/providers/ApplicationsProvider/hooks.ts b/client/src/providers/ApplicationsProvider/hooks.ts index 962161cff..909598d5c 100644 --- a/client/src/providers/ApplicationsProvider/hooks.ts +++ b/client/src/providers/ApplicationsProvider/hooks.ts @@ -1,9 +1,9 @@ -import { useContext } from 'react' +import { use } from 'react' import { ApplicationsContext } from './context' -import { IApplication } from '../../requests/responses/application' +import type { IApplication } from '../../requests/responses/application' export function useApplicationsContext() { - const data = useContext(ApplicationsContext) + const data = use(ApplicationsContext) if (!data) { throw new Error('ApplicationsContext not initialized') @@ -13,7 +13,7 @@ export function useApplicationsContext() { } export function useApplicationsContextUpdater(): (application: IApplication) => unknown { - const data = useContext(ApplicationsContext) + const data = use(ApplicationsContext) if (!data) { return () => undefined diff --git a/client/src/providers/AuthenticationContext/AuthenticationProvider.tsx b/client/src/providers/AuthenticationContext/AuthenticationProvider.tsx index 002ded176..1a7d4a79d 100644 --- a/client/src/providers/AuthenticationContext/AuthenticationProvider.tsx +++ b/client/src/providers/AuthenticationContext/AuthenticationProvider.tsx @@ -1,20 +1,17 @@ -import { PropsWithChildren, useEffect, useMemo, useState } from 'react' -import { - AuthenticationContext, - IAuthenticationContext, - IDecodedAccessToken, - IDecodedRefreshToken, -} from './context' +import type { PropsWithChildren } from 'react' +import { useEffect, useMemo, useState } from 'react' +import type { IAuthenticationContext, IDecodedAccessToken, IDecodedRefreshToken } from './context' +import { AuthenticationContext } from './context' import Keycloak from 'keycloak-js' import { GLOBAL_CONFIG } from '../../config/global' import { jwtDecode } from 'jwt-decode' import { getAuthenticationTokens, useAuthenticationTokens } from '../../hooks/authentication' import { useSignal } from '../../hooks/utility' -import { IUser } from '../../requests/responses/user' +import type { IUser } from '../../requests/responses/user' import { doRequest } from '../../requests/request' import { showSimpleError } from '../../utils/notification' import { ApiError, getApiResponseErrorMessage } from '../../requests/handler' -import { ILightResearchGroup } from '../../requests/responses/researchGroup' +import type { ILightResearchGroup } from '../../requests/responses/researchGroup' export const keycloak = new Keycloak({ realm: GLOBAL_CONFIG.keycloak.realm, @@ -39,7 +36,7 @@ const AuthenticationProvider = (props: PropsWithChildren) => { setUser(undefined) const refreshAccessToken = () => { - keycloak.updateToken(60 * 5).then((isSuccess) => { + void keycloak.updateToken(60 * 5).then((isSuccess) => { if (!isSuccess) { setAuthenticationTokens(undefined) } @@ -57,15 +54,6 @@ const AuthenticationProvider = (props: PropsWithChildren) => { ? jwtDecode(refreshToken) : undefined - console.log('decoded keycloak refresh token', decodedRefreshToken) - console.log('decoded keycloak access token', decodedAccessToken) - - if (decodedRefreshToken?.exp) { - console.log( - `refresh token expires in ${Math.floor(decodedRefreshToken.exp - Date.now() / 1000)} seconds`, - ) - } - // refresh if already expired if (decodedRefreshToken?.exp && decodedRefreshToken.exp <= Date.now() / 1000) { return setAuthenticationTokens(undefined) @@ -95,21 +83,17 @@ const AuthenticationProvider = (props: PropsWithChildren) => { setAuthenticationTokens(undefined) } - console.log('Initializing keycloak...') - void keycloak .init({ refreshToken: storedTokens?.refresh_token, token: storedTokens?.access_token, }) .then(() => { - console.log('Keycloak initialized') - storeTokens() triggerReadySignal() }) .catch((error) => { - console.log('Keycloak init error', error) + console.error('Keycloak init error', error) }) const refreshTokenFrequency = 60 * 1000 @@ -134,6 +118,7 @@ const AuthenticationProvider = (props: PropsWithChildren) => { keycloak.onAuthRefreshError = undefined keycloak.onAuthLogout = undefined } + // eslint-disable-next-line @eslint-react/exhaustive-deps -- mount-only keycloak setup: setAuthenticationTokens/triggerReadySignal are stable refs intentionally captured once }, []) useEffect(() => { @@ -193,9 +178,11 @@ const AuthenticationProvider = (props: PropsWithChildren) => { ) }, [isReady, universityId]) + const isAuthenticated = Boolean(authenticationTokens?.access_token) + const contextValue = useMemo(() => { return { - isAuthenticated: !!authenticationTokens?.access_token, + isAuthenticated: Boolean(authenticationTokens?.access_token), user: authenticationTokens?.access_token ? user : undefined, groups: [], updateUser: setUser, @@ -209,7 +196,7 @@ const AuthenticationProvider = (props: PropsWithChildren) => { } if (examinationReport) { - formData.append('examinationReport', examinationReport!) + formData.append('examinationReport', examinationReport) } if (cv) { @@ -247,7 +234,7 @@ const AuthenticationProvider = (props: PropsWithChildren) => { window.location.href = `${window.location.origin}${redirectUri}` }, 2000) - readySignal.then(() => { + void readySignal.then(() => { if (keycloak.authenticated) { clearTimeout(timeout) @@ -259,16 +246,10 @@ const AuthenticationProvider = (props: PropsWithChildren) => { }, researchGroups: researchGroups, } - }, [ - user, - !!authenticationTokens?.access_token, - authenticationTokens?.refresh_token, - location.origin, - ]) - - return ( - {children} - ) + // eslint-disable-next-line @eslint-react/exhaustive-deps -- researchGroups/setAuthenticationTokens/readySignal/access_token are captured by reference inside callbacks that read the latest value at call time; recomputing the entire context on each token refresh would re-render every consumer + }, [user, isAuthenticated, authenticationTokens?.refresh_token, researchGroups]) + + return {children} } export default AuthenticationProvider diff --git a/client/src/providers/AuthenticationContext/context.ts b/client/src/providers/AuthenticationContext/context.ts index 3d224e54d..8fee45bc4 100644 --- a/client/src/providers/AuthenticationContext/context.ts +++ b/client/src/providers/AuthenticationContext/context.ts @@ -1,9 +1,9 @@ import { createContext } from 'react' -import { JwtPayload } from 'jwt-decode' -import { IUser } from '../../requests/responses/user' -import { IUpdateUserInformationPayload } from '../../requests/payloads/user' -import { PartialNull } from '../../utils/validation' -import { ILightResearchGroup } from '../../requests/responses/researchGroup' +import type { JwtPayload } from 'jwt-decode' +import type { IUser } from '../../requests/responses/user' +import type { IUpdateUserInformationPayload } from '../../requests/payloads/user' +import type { PartialNull } from '../../utils/validation' +import type { ILightResearchGroup } from '../../requests/responses/researchGroup' export interface IAuthenticationContext { isAuthenticated: boolean @@ -30,9 +30,9 @@ export interface IDecodedAccessToken extends JwtPayload { email: string preferred_username: string resource_access: Partial> - [key: string]: any + [key: string]: unknown } export interface IDecodedRefreshToken extends JwtPayload { - [key: string]: any + [key: string]: unknown } diff --git a/client/src/providers/InterviewProcessProvider/InterviewProcessProvider.tsx b/client/src/providers/InterviewProcessProvider/InterviewProcessProvider.tsx index cf206ab8d..5192e2c4d 100644 --- a/client/src/providers/InterviewProcessProvider/InterviewProcessProvider.tsx +++ b/client/src/providers/InterviewProcessProvider/InterviewProcessProvider.tsx @@ -1,10 +1,15 @@ -import React, { PropsWithChildren, useCallback, useEffect, useMemo, useState } from 'react' -import { InterviewProcessContext, IInterviewProcessContext } from './context' +import type { PropsWithChildren } from 'react' +import React, { useCallback, useEffect, useMemo, useState } from 'react' +import type { IInterviewProcessContext } from './context' +import { InterviewProcessContext } from './context' import { doRequest } from '../../requests/request' import { showSimpleError } from '../../utils/notification' import { getApiResponseErrorMessage } from '../../requests/handler' -import { IIntervieweeLightWithNextSlot, IInterviewSlot } from '../../requests/responses/interview' -import { PaginationResponse } from '../../requests/responses/pagination' +import type { + IIntervieweeLightWithNextSlot, + IInterviewSlot, +} from '../../requests/responses/interview' +import type { PaginationResponse } from '../../requests/responses/pagination' import { useParams } from 'react-router' interface IInterviewProcessProviderProps { @@ -66,6 +71,7 @@ const InterviewProcessProvider = (props: PropsWithChildren((resolve) => { - doRequest( + doRequest( `/v2/interview-process/${processId}/interviewees`, { method: 'POST', @@ -183,7 +191,7 @@ const InterviewProcessProvider = (props: PropsWithChildren { if (res.ok) { - fetchPossibleInterviewees() // TODO: Missing searchkey and state? + void fetchPossibleInterviewees() // TODO: Missing searchkey and state? } else { showSimpleError(getApiResponseErrorMessage(res)) resolve() @@ -204,8 +212,9 @@ const InterviewProcessProvider = (props: PropsWithChildren(() => { @@ -239,6 +248,7 @@ const InterviewProcessProvider = (props: PropsWithChildren - {children} - - ) + return {children} } export default InterviewProcessProvider diff --git a/client/src/providers/InterviewProcessProvider/context.ts b/client/src/providers/InterviewProcessProvider/context.ts index 486ecda19..f9c55cfcf 100644 --- a/client/src/providers/InterviewProcessProvider/context.ts +++ b/client/src/providers/InterviewProcessProvider/context.ts @@ -1,5 +1,9 @@ -import React, { Dispatch, SetStateAction } from 'react' -import { IIntervieweeLightWithNextSlot, IInterviewSlot } from '../../requests/responses/interview' // adjust path if needed +import type { Dispatch, SetStateAction } from 'react' +import React from 'react' +import type { + IIntervieweeLightWithNextSlot, + IInterviewSlot, +} from '../../requests/responses/interview' // adjust path if needed export interface IInterviewProcessContext { processId: string | undefined diff --git a/client/src/providers/InterviewProcessProvider/hooks.ts b/client/src/providers/InterviewProcessProvider/hooks.ts index 2ae4c912a..e9bfbe7e8 100644 --- a/client/src/providers/InterviewProcessProvider/hooks.ts +++ b/client/src/providers/InterviewProcessProvider/hooks.ts @@ -1,8 +1,8 @@ -import { useContext } from 'react' +import { use } from 'react' import { InterviewProcessContext } from './context' export function useInterviewProcessContext() { - const data = useContext(InterviewProcessContext) + const data = use(InterviewProcessContext) if (!data) { throw new Error('InterviewProcessContext not initialized') diff --git a/client/src/providers/ThesesProvider/ThesesProvider.tsx b/client/src/providers/ThesesProvider/ThesesProvider.tsx index 4a0da5a46..3c5b4106e 100644 --- a/client/src/providers/ThesesProvider/ThesesProvider.tsx +++ b/client/src/providers/ThesesProvider/ThesesProvider.tsx @@ -1,8 +1,10 @@ -import React, { PropsWithChildren, useEffect, useMemo, useState } from 'react' -import { ThesesContext, IThesesContext, IThesesFilters, IThesesSort } from './context' -import { IThesisOverview, ThesisState } from '../../requests/responses/thesis' +import type { PropsWithChildren } from 'react' +import React, { useEffect, useMemo, useState } from 'react' +import type { IThesesContext, IThesesFilters, IThesesSort } from './context' +import { ThesesContext } from './context' +import type { IThesisOverview, ThesisState } from '../../requests/responses/thesis' import { doRequest } from '../../requests/request' -import { PaginationResponse } from '../../requests/responses/pagination' +import type { PaginationResponse } from '../../requests/responses/pagination' import { useDebouncedValue } from '@mantine/hooks' import { showSimpleError } from '../../utils/notification' import { getApiResponseErrorMessage } from '../../requests/handler' @@ -28,7 +30,10 @@ const ThesesProvider = (props: PropsWithChildren) => { direction: 'asc', }) - const [debouncedSearch] = useDebouncedValue(filters.search || '', 500) + const [debouncedSearch] = useDebouncedValue(filters.search ?? '', 500) + + const filterStatesKey = filters.states?.join(',') + const filterTypesKey = filters.types?.join(',') useEffect(() => { setTheses(undefined) @@ -66,15 +71,8 @@ const ThesesProvider = (props: PropsWithChildren) => { setTheses(res.data) }, ) - }, [ - fetchAll, - page, - limit, - sort, - filters.states?.join(','), - filters.types?.join(','), - debouncedSearch, - ]) + // eslint-disable-next-line @eslint-react/exhaustive-deps -- filters.states/types are tracked via joined keys below to avoid identity-based reruns + }, [fetchAll, page, limit, sort, filterStatesKey, filterTypesKey, debouncedSearch]) const contextState = useMemo(() => { return { @@ -117,7 +115,7 @@ const ThesesProvider = (props: PropsWithChildren) => { return <> } - return {children} + return {children} } export default ThesesProvider diff --git a/client/src/providers/ThesesProvider/context.ts b/client/src/providers/ThesesProvider/context.ts index f728bd5f9..2e3ff17f8 100644 --- a/client/src/providers/ThesesProvider/context.ts +++ b/client/src/providers/ThesesProvider/context.ts @@ -1,6 +1,7 @@ -import React, { Dispatch, SetStateAction } from 'react' -import { IThesisOverview, ThesisState } from '../../requests/responses/thesis' -import { PaginationResponse } from '../../requests/responses/pagination' +import type { Dispatch, SetStateAction } from 'react' +import React from 'react' +import type { IThesisOverview, ThesisState } from '../../requests/responses/thesis' +import type { PaginationResponse } from '../../requests/responses/pagination' export interface IThesesFilters { search?: string diff --git a/client/src/providers/ThesesProvider/hooks.ts b/client/src/providers/ThesesProvider/hooks.ts index c18cd76ef..340e84808 100644 --- a/client/src/providers/ThesesProvider/hooks.ts +++ b/client/src/providers/ThesesProvider/hooks.ts @@ -1,8 +1,8 @@ -import { useContext } from 'react' +import { use } from 'react' import { ThesesContext } from './context' export function useThesesContext() { - const data = useContext(ThesesContext) + const data = use(ThesesContext) if (!data) { throw new Error('ThesesContext not initialized') diff --git a/client/src/providers/ThesisCommentsProvider/ThesisCommentsProvider.tsx b/client/src/providers/ThesisCommentsProvider/ThesisCommentsProvider.tsx index 560e373c8..2adce8476 100644 --- a/client/src/providers/ThesisCommentsProvider/ThesisCommentsProvider.tsx +++ b/client/src/providers/ThesisCommentsProvider/ThesisCommentsProvider.tsx @@ -1,7 +1,9 @@ -import React, { PropsWithChildren, useEffect, useMemo, useState } from 'react' -import { IThesis, IThesisComment } from '../../requests/responses/thesis' -import { IThesisCommentsContext, ThesisCommentsContext } from './context' -import { PaginationResponse } from '../../requests/responses/pagination' +import type { PropsWithChildren } from 'react' +import React, { useEffect, useMemo, useState } from 'react' +import type { IThesis, IThesisComment } from '../../requests/responses/thesis' +import type { IThesisCommentsContext } from './context' +import { ThesisCommentsContext } from './context' +import type { PaginationResponse } from '../../requests/responses/pagination' import { doRequest } from '../../requests/request' import { showSimpleError } from '../../utils/notification' import { getApiResponseErrorMessage } from '../../requests/handler' @@ -148,9 +150,9 @@ const ThesisCommentsProvider = (props: PropsWithChildren + {children} - + ) } diff --git a/client/src/providers/ThesisCommentsProvider/context.ts b/client/src/providers/ThesisCommentsProvider/context.ts index 627e6c295..72b8d1197 100644 --- a/client/src/providers/ThesisCommentsProvider/context.ts +++ b/client/src/providers/ThesisCommentsProvider/context.ts @@ -1,6 +1,7 @@ -import { IThesis, IThesisComment } from '../../requests/responses/thesis' -import React, { Dispatch, SetStateAction } from 'react' -import { PaginationResponse } from '../../requests/responses/pagination' +import type { IThesis, IThesisComment } from '../../requests/responses/thesis' +import type { Dispatch, SetStateAction } from 'react' +import React from 'react' +import type { PaginationResponse } from '../../requests/responses/pagination' export interface IThesisCommentsContext { thesis: IThesis diff --git a/client/src/providers/ThesisCommentsProvider/hooks.ts b/client/src/providers/ThesisCommentsProvider/hooks.ts index f7b73fcd5..c7170f5b1 100644 --- a/client/src/providers/ThesisCommentsProvider/hooks.ts +++ b/client/src/providers/ThesisCommentsProvider/hooks.ts @@ -1,8 +1,8 @@ -import { useContext } from 'react' +import { use } from 'react' import { ThesisCommentsContext } from './context' export function useThesisCommentsContext() { - const data = useContext(ThesisCommentsContext) + const data = use(ThesisCommentsContext) if (!data) { throw new Error('ThesisCommentsContext not initialized') diff --git a/client/src/providers/ThesisProvider/ThesisProvider.tsx b/client/src/providers/ThesisProvider/ThesisProvider.tsx index c2c48c211..4471b05a6 100644 --- a/client/src/providers/ThesisProvider/ThesisProvider.tsx +++ b/client/src/providers/ThesisProvider/ThesisProvider.tsx @@ -1,7 +1,9 @@ -import React, { PropsWithChildren, useEffect, useMemo, useState } from 'react' -import { IThesis } from '../../requests/responses/thesis' +import type { PropsWithChildren } from 'react' +import React, { useEffect, useMemo, useState } from 'react' +import type { IThesis } from '../../requests/responses/thesis' import { useThesis } from '../../hooks/fetcher' -import { IThesisContext, ThesisContext } from './context' +import type { IThesisContext } from './context' +import { ThesisContext } from './context' import NotFound from '../../components/NotFound/NotFound' import PageLoader from '../../components/PageLoader/PageLoader' import { useThesisAccess } from './hooks' @@ -49,9 +51,9 @@ const ThesisProvider = (props: PropsWithChildren) => { } return ( - + {children} - + ) } diff --git a/client/src/providers/ThesisProvider/context.ts b/client/src/providers/ThesisProvider/context.ts index 3dd599699..ada93dc35 100644 --- a/client/src/providers/ThesisProvider/context.ts +++ b/client/src/providers/ThesisProvider/context.ts @@ -1,4 +1,4 @@ -import { IThesis } from '../../requests/responses/thesis' +import type { IThesis } from '../../requests/responses/thesis' import React from 'react' export interface IThesisContext { diff --git a/client/src/providers/ThesisProvider/hooks.ts b/client/src/providers/ThesisProvider/hooks.ts index 0ad61cb97..647486ac6 100644 --- a/client/src/providers/ThesisProvider/hooks.ts +++ b/client/src/providers/ThesisProvider/hooks.ts @@ -1,11 +1,11 @@ -import { useContext, useMemo, useState } from 'react' +import { use, useMemo, useState } from 'react' import { ThesisContext } from './context' -import { IPublishedThesis, IThesis } from '../../requests/responses/thesis' +import type { IPublishedThesis, IThesis } from '../../requests/responses/thesis' import { showSimpleError, showSimpleSuccess } from '../../utils/notification' import { useUser } from '../../hooks/authentication' export function useThesisContext() { - const data = useContext(ThesisContext) + const data = use(ThesisContext) if (!data) { throw new Error('ThesisContext not initialized') @@ -25,7 +25,7 @@ export function useLoadedThesisContext() { } export function useThesisContextUpdater() { - const data = useContext(ThesisContext) + const data = use(ThesisContext) if (!data) { return () => undefined @@ -34,6 +34,7 @@ export function useThesisContextUpdater() { return data.updateThesis } +// eslint-disable-next-line @typescript-eslint/no-explicit-any -- generic constraint must accept any function shape so callers can pass functions with arbitrary parameter types; narrowing to `unknown[]` would reject all real callers export function useThesisUpdateAction any>( fn: (...args: Parameters) => PromiseLike, successMessage?: string, diff --git a/client/src/providers/TopicsProvider/TopicsProvider.tsx b/client/src/providers/TopicsProvider/TopicsProvider.tsx index 6748d9c71..a5d941e3d 100644 --- a/client/src/providers/TopicsProvider/TopicsProvider.tsx +++ b/client/src/providers/TopicsProvider/TopicsProvider.tsx @@ -1,9 +1,12 @@ -import React, { PropsWithChildren, useEffect, useMemo, useRef, useState } from 'react' +import type { PropsWithChildren } from 'react' +import React, { useEffect, useMemo, useRef, useState } from 'react' import { doRequest } from '../../requests/request' import { showSimpleError } from '../../utils/notification' -import { ITopicOverview, TopicState } from '../../requests/responses/topic' -import { ITopicsContext, ITopicsFilters, TopicsContext } from './context' -import { PaginationResponse } from '../../requests/responses/pagination' +import type { ITopicOverview } from '../../requests/responses/topic' +import { TopicState } from '../../requests/responses/topic' +import type { ITopicsContext, ITopicsFilters } from './context' +import { TopicsContext } from './context' +import type { PaginationResponse } from '../../requests/responses/pagination' interface ITopicsProviderProps { limit: number @@ -44,11 +47,11 @@ const TopicsProvider = (props: PropsWithChildren) => { params: { page, limit, - type: filters.types?.join(',') || '', - states: filters.states?.join(',') || '', + type: filters.types?.join(',') ?? '', + states: filters.states?.join(',') ?? '', onlyOwnResearchGroup: filters.researchSpecific ? 'true' : 'false', search: filters.search ?? '', - researchGroupIds: filters.researchGroupIds?.join(',') || '', + researchGroupIds: filters.researchGroupIds?.join(',') ?? '', }, }, (res) => { @@ -74,6 +77,7 @@ const TopicsProvider = (props: PropsWithChildren) => { useEffect(() => { return fetchTopics() + // eslint-disable-next-line @eslint-react/exhaustive-deps -- fetchTopics is recreated each render; only refetch when filters/pagination change }, [filters, page, limit]) const initialFiltersKey = JSON.stringify(initialFilters) @@ -92,6 +96,7 @@ const TopicsProvider = (props: PropsWithChildren) => { ...initialFilters, })) setPage(0) + // eslint-disable-next-line @eslint-react/exhaustive-deps -- initialFilters/states/researchSpecific are captured at the time initialFiltersKey changes; tracking the raw values would re-run on every render }, [initialFiltersKey]) const contextState = useMemo(() => { @@ -147,13 +152,14 @@ const TopicsProvider = (props: PropsWithChildren) => { }) }, } + // eslint-disable-next-line @eslint-react/exhaustive-deps -- fetchTopics is recreated each render and is only called from within callbacks at invocation time }, [topics, filters, page, limit, isLoading]) if (hideIfEmpty && page === 0 && (!topics || (topics.content?.length ?? 0) === 0)) { return <> } - return {children} + return {children} } export default TopicsProvider diff --git a/client/src/providers/TopicsProvider/context.ts b/client/src/providers/TopicsProvider/context.ts index d52dafd3f..5aec50437 100644 --- a/client/src/providers/TopicsProvider/context.ts +++ b/client/src/providers/TopicsProvider/context.ts @@ -1,6 +1,7 @@ -import React, { Dispatch, SetStateAction } from 'react' -import { ITopicOverview } from '../../requests/responses/topic' -import { PaginationResponse } from '../../requests/responses/pagination' +import type { Dispatch, SetStateAction } from 'react' +import React from 'react' +import type { ITopicOverview } from '../../requests/responses/topic' +import type { PaginationResponse } from '../../requests/responses/pagination' export interface ITopicsFilters { types?: string[] diff --git a/client/src/providers/TopicsProvider/hooks.ts b/client/src/providers/TopicsProvider/hooks.ts index 977da7e83..7430643df 100644 --- a/client/src/providers/TopicsProvider/hooks.ts +++ b/client/src/providers/TopicsProvider/hooks.ts @@ -1,8 +1,8 @@ -import { useContext } from 'react' +import { use } from 'react' import { TopicsContext } from './context' export function useTopicsContext() { - const data = useContext(TopicsContext) + const data = use(TopicsContext) if (!data) { throw new Error('TopicsContext not initialized') diff --git a/client/src/requests/handler.ts b/client/src/requests/handler.ts index 607151efc..a428e9bdc 100644 --- a/client/src/requests/handler.ts +++ b/client/src/requests/handler.ts @@ -1,4 +1,4 @@ -import { ApiResponse } from './request' +import type { ApiResponse } from './request' export function getApiResponseErrorMessage(response: ApiResponse) { if (response.status === 1005) { @@ -19,8 +19,6 @@ export function getApiResponseErrorMessage(response: ApiResponse) { message = 'You are not authorized to access this resource' } else if (response.status === 404) { message = 'Requested resource not found' - } else if (response.status === 404) { - message = 'Requested resource not found' } else if (response.status === 409) { message = 'Resource already exists' } else if (response.status === 501) { diff --git a/client/src/requests/request.ts b/client/src/requests/request.ts index 9e256e594..ce3649a27 100644 --- a/client/src/requests/request.ts +++ b/client/src/requests/request.ts @@ -11,7 +11,7 @@ export interface IRequestOptions { method: HttpMethod requiresAuth: boolean responseType?: ResponseType - data?: any + data?: unknown formData?: FormData params?: Record controller?: AbortController @@ -28,7 +28,7 @@ export function doRequest( options: IRequestOptions, cb?: (res: ApiResponse) => unknown, ): Promise> | (() => void) { - const controller = options.controller || new AbortController() + const controller = options.controller ?? new AbortController() const executeRequest = async (): Promise> => { if (options.requiresAuth && keycloak.isTokenExpired(5)) { @@ -105,7 +105,7 @@ export function doRequest( const blacklistedCodes = [1005] if (cb) { - promise.then((res) => !blacklistedCodes.includes(res.status) && cb(res)) + void promise.then((res) => !blacklistedCodes.includes(res.status) && cb(res)) return () => { controller.abort() diff --git a/client/src/requests/responses/application.ts b/client/src/requests/responses/application.ts index a7e549597..d8b522e38 100644 --- a/client/src/requests/responses/application.ts +++ b/client/src/requests/responses/application.ts @@ -1,6 +1,6 @@ -import { ILightUser, IUser } from './user' -import { ITopic } from './topic' -import { ILightResearchGroup } from './researchGroup' +import type { ILightUser, IUser } from './user' +import type { ITopic } from './topic' +import type { ILightResearchGroup } from './researchGroup' export enum ApplicationState { NOT_ASSESSED = 'NOT_ASSESSED', diff --git a/client/src/requests/responses/emailtemplate.ts b/client/src/requests/responses/emailtemplate.ts index 8712178c5..ad1865864 100644 --- a/client/src/requests/responses/emailtemplate.ts +++ b/client/src/requests/responses/emailtemplate.ts @@ -1,4 +1,4 @@ -import { ILightResearchGroup } from './researchGroup' +import type { ILightResearchGroup } from './researchGroup' export interface IEmailTemplate { id: string diff --git a/client/src/requests/responses/interview.ts b/client/src/requests/responses/interview.ts index d6b8d21d7..771d9e72f 100644 --- a/client/src/requests/responses/interview.ts +++ b/client/src/requests/responses/interview.ts @@ -1,5 +1,5 @@ -import { ApplicationState, IApplicationSummary } from './application' -import { ILightUser } from './user' +import type { ApplicationState, IApplicationSummary } from './application' +import type { ILightUser } from './user' export enum InterviewState { UNCONTACTED = 'Uncontacted', diff --git a/client/src/requests/responses/researchGroup.ts b/client/src/requests/responses/researchGroup.ts index a0f68f319..fb0dc9416 100644 --- a/client/src/requests/responses/researchGroup.ts +++ b/client/src/requests/responses/researchGroup.ts @@ -1,4 +1,4 @@ -import { ILightUser } from './user' +import type { ILightUser } from './user' export interface IMinimalResearchGroup { id: string diff --git a/client/src/requests/responses/thesis.ts b/client/src/requests/responses/thesis.ts index d0ff62c71..b03ef3f52 100644 --- a/client/src/requests/responses/thesis.ts +++ b/client/src/requests/responses/thesis.ts @@ -1,5 +1,5 @@ -import { ILightResearchGroup, IMinimalResearchGroup } from './researchGroup' -import { ILightUser, IMinimalUser } from './user' +import type { ILightResearchGroup, IMinimalResearchGroup } from './researchGroup' +import type { ILightUser, IMinimalUser } from './user' export enum ThesisState { PROPOSAL = 'PROPOSAL', @@ -152,14 +152,22 @@ export interface IPublishedPresentation { thesis: IPublishedThesis } -export function isThesis(thesis: any): thesis is IThesis { - return thesis.thesisId && !!thesis.states && 'language' in thesis +export function isThesis(thesis: unknown): thesis is IThesis { + if (!thesis || typeof thesis !== 'object') return false + const obj = thesis as Record + return !!obj.thesisId && !!obj.states && 'language' in obj } -export function isThesisPresentation(presentation: any): presentation is IThesisPresentation { - return presentation.presentationId && !presentation.thesis +export function isThesisPresentation(presentation: unknown): presentation is IThesisPresentation { + if (!presentation || typeof presentation !== 'object') return false + const obj = presentation as Record + return !!obj.presentationId && !obj.thesis } -export function isPublishedPresentation(presentation: any): presentation is IPublishedPresentation { - return presentation.presentationId && presentation.thesis +export function isPublishedPresentation( + presentation: unknown, +): presentation is IPublishedPresentation { + if (!presentation || typeof presentation !== 'object') return false + const obj = presentation as Record + return !!obj.presentationId && !!obj.thesis } diff --git a/client/src/requests/responses/topic.ts b/client/src/requests/responses/topic.ts index d4aeb602d..6dac20708 100644 --- a/client/src/requests/responses/topic.ts +++ b/client/src/requests/responses/topic.ts @@ -1,5 +1,5 @@ -import { ILightResearchGroup, IMinimalResearchGroup } from './researchGroup' -import { ILightUser, IMinimalUser } from './user' +import type { ILightResearchGroup, IMinimalResearchGroup } from './researchGroup' +import type { ILightUser, IMinimalUser } from './user' export enum TopicState { OPEN = 'OPEN', diff --git a/client/src/utils/customDataLink.tsx b/client/src/utils/customDataLink.tsx index a6705f328..4f62b8989 100644 --- a/client/src/utils/customDataLink.tsx +++ b/client/src/utils/customDataLink.tsx @@ -1,5 +1,5 @@ import { Anchor, Text } from '@mantine/core' -import { ReactNode } from 'react' +import type { ReactNode } from 'react' // GitHub username rules (from the GitHub UI): // - 1-39 characters diff --git a/client/src/utils/file.ts b/client/src/utils/file.ts index 95752495a..be0aeb768 100644 --- a/client/src/utils/file.ts +++ b/client/src/utils/file.ts @@ -1,4 +1,4 @@ -import { UploadFileType } from '../config/types' +import type { UploadFileType } from '../config/types' export function getAdjustedFileType(filename: string, type: UploadFileType) { let adjustedType: UploadFileType = type diff --git a/client/src/utils/format.ts b/client/src/utils/format.ts index 2df0b0426..00c4cc91a 100644 --- a/client/src/utils/format.ts +++ b/client/src/utils/format.ts @@ -1,9 +1,10 @@ -import { ILightUser, IMinimalUser } from '../requests/responses/user' -import { IThesis, ThesisState } from '../requests/responses/thesis' -import { ApplicationState, IApplication } from '../requests/responses/application' +import type { ILightUser, IMinimalUser } from '../requests/responses/user' +import type { IThesis } from '../requests/responses/thesis' +import { ThesisState } from '../requests/responses/thesis' +import type { IApplication } from '../requests/responses/application' +import { ApplicationState } from '../requests/responses/application' import { GLOBAL_CONFIG } from '../config/global' import { InterviewState } from '../requests/responses/interview' -import { useMantineColorScheme } from '@mantine/core' import { TopicState } from '../requests/responses/topic' interface IFormatDateOptions { @@ -256,9 +257,7 @@ export function createInterviewStageLabel(score: number): string { } } -export function getInterviewStateColor(state: InterviewState): string { - const colorScheme = useMantineColorScheme() - +export function getInterviewStateColor(state: InterviewState, isDark: boolean): string { switch (state) { case InterviewState.UNCONTACTED: return 'primary.1' @@ -267,7 +266,7 @@ export function getInterviewStateColor(state: InterviewState): string { case InterviewState.SCHEDULED: return 'primary.5' case InterviewState.COMPLETED: - return colorScheme.colorScheme === 'dark' ? 'primary.8' : 'primary.10' + return isDark ? 'primary.8' : 'primary.10' default: return 'gray' } diff --git a/client/src/utils/thesis.ts b/client/src/utils/thesis.ts index c0db2b423..48c4f9b96 100644 --- a/client/src/utils/thesis.ts +++ b/client/src/utils/thesis.ts @@ -1,5 +1,6 @@ -import { IPublishedThesis, IThesis, ThesisState } from '../requests/responses/thesis' -import { ILightUser } from '../requests/responses/user' +import type { IPublishedThesis, IThesis } from '../requests/responses/thesis' +import { ThesisState } from '../requests/responses/thesis' +import type { ILightUser } from '../requests/responses/user' export function isThesisClosed(thesis: IThesis | IPublishedThesis) { return thesis.state === ThesisState.FINISHED || thesis.state === ThesisState.DROPPED_OUT @@ -23,9 +24,9 @@ export function hasStudentAccess( ...(thesis.examiners ?? []), ] - return !!( + return Boolean( users.some((row) => row.userId === user?.userId) || - user?.groups?.some((name) => name === 'admin') + user?.groups?.some((name) => name === 'admin'), ) } @@ -39,9 +40,9 @@ export function hasSupervisorAccess( const users = [...(thesis.supervisors ?? []), ...(thesis.examiners ?? [])] - return !!( + return Boolean( users.some((row) => row.userId === user?.userId) || - user?.groups?.some((name) => name === 'admin') + user?.groups?.some((name) => name === 'admin'), ) } @@ -49,8 +50,8 @@ export function hasExaminerAccess( thesis: IPublishedThesis | undefined, user: ILightUser | undefined, ) { - return !!( + return Boolean( (thesis?.examiners ?? []).some((row) => row.userId === user?.userId) || - user?.groups?.some((name) => name === 'admin') + user?.groups?.some((name) => name === 'admin'), ) } diff --git a/client/src/utils/user.ts b/client/src/utils/user.ts index 08013281c..55becd4e0 100644 --- a/client/src/utils/user.ts +++ b/client/src/utils/user.ts @@ -1,5 +1,5 @@ import { GLOBAL_CONFIG } from '../config/global' -import { IMinimalUser } from '../requests/responses/user' +import type { IMinimalUser } from '../requests/responses/user' export function getAvatar(user: IMinimalUser) { return user.avatar diff --git a/client/test/render.tsx b/client/test/render.tsx index 40ef04957..1ca3630b2 100644 --- a/client/test/render.tsx +++ b/client/test/render.tsx @@ -1,6 +1,7 @@ -import { ReactElement, ReactNode } from 'react' +import type { ReactElement, ReactNode } from 'react' import { MantineProvider } from '@mantine/core' -import { render, RenderOptions, RenderResult } from '@testing-library/react' +import type { RenderOptions, RenderResult } from '@testing-library/react' +import { render } from '@testing-library/react' interface ProvidersProps { children: ReactNode diff --git a/client/test/setup.ts b/client/test/setup.ts index d9cbb00cc..52ab68594 100644 --- a/client/test/setup.ts +++ b/client/test/setup.ts @@ -30,7 +30,7 @@ class ResizeObserverMock { disconnect(): void {} } if (!('ResizeObserver' in globalThis)) { - globalThis.ResizeObserver = ResizeObserverMock as unknown as typeof ResizeObserver + globalThis.ResizeObserver = ResizeObserverMock } // 3) scrollTo — Mantine occasionally calls window.scrollTo and element.scrollTo.