diff --git a/commafeed-client/.eslintignore b/commafeed-client/.eslintignore deleted file mode 100644 index a3068c1b..00000000 --- a/commafeed-client/.eslintignore +++ /dev/null @@ -1,8 +0,0 @@ -dist -node_modules - -vite.config.ts - -# compiled linguijs locales -# they no longer exist but we keep this to avoid issues with people still having those files on disk -src/locales/**/*.ts \ No newline at end of file diff --git a/commafeed-client/.eslintrc.cjs b/commafeed-client/.eslintrc.cjs deleted file mode 100644 index 579f7751..00000000 --- a/commafeed-client/.eslintrc.cjs +++ /dev/null @@ -1,52 +0,0 @@ -module.exports = { - env: { - browser: true, - es2021: true, - }, - extends: [ - "eslint:recommended", - "standard", - "love", - "plugin:@typescript-eslint/strict-type-checked", - "plugin:@typescript-eslint/stylistic-type-checked", - "plugin:react/recommended", - "plugin:react-hooks/recommended", - "plugin:prettier/recommended", - ], - settings: { - react: { - version: "detect", - }, - }, - overrides: [ - { - env: { - node: true, - }, - files: [".eslintrc.{js,cjs}"], - parserOptions: { - sourceType: "script", - }, - }, - ], - parserOptions: { - project: true, - ecmaVersion: "latest", - sourceType: "module", - }, - plugins: ["react"], - rules: { - "@typescript-eslint/consistent-type-assertions": ["error", { assertionStyle: "as" }], - "@typescript-eslint/explicit-function-return-type": "off", - "@typescript-eslint/no-confusing-void-expression": ["error", { ignoreArrowShorthand: true }], - "@typescript-eslint/no-floating-promises": "off", - "@typescript-eslint/no-misused-promises": "off", - "@typescript-eslint/prefer-nullish-coalescing": ["error", { ignoreConditionalTests: true }], - "@typescript-eslint/restrict-template-expressions": ["error", { allowNumber: true }], - "@typescript-eslint/strict-boolean-expressions": "off", - "react/jsx-curly-brace-presence": ["error", "never"], - "react/no-unescaped-entities": "off", - "react/react-in-jsx-scope": "off", - "react-hooks/exhaustive-deps": "error", - }, -} diff --git a/commafeed-client/.prettierrc b/commafeed-client/.prettierrc deleted file mode 100644 index ff8e2b00..00000000 --- a/commafeed-client/.prettierrc +++ /dev/null @@ -1,8 +0,0 @@ -{ - "printWidth": 140, - "semi": false, - "tabWidth": 4, - "arrowParens": "avoid", - "endOfLine": "auto", - "trailingComma": "es5" -} diff --git a/commafeed-client/biome.json b/commafeed-client/biome.json new file mode 100644 index 00000000..788e4233 --- /dev/null +++ b/commafeed-client/biome.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.8.1/schema.json", + "formatter": { + "indentStyle": "space", + "indentWidth": 4, + "lineEnding": "lf", + "lineWidth": 140 + }, + "javascript": { + "formatter": { + "trailingCommas": "es5", + "semicolons": "asNeeded", + "arrowParentheses": "asNeeded" + } + }, + "files": { + "ignore": ["dist", "node_modules", "target", "target-ide"] + } +} diff --git a/commafeed-client/package-lock.json b/commafeed-client/package-lock.json index 058d177c..260ce76f 100644 --- a/commafeed-client/package-lock.json +++ b/commafeed-client/package-lock.json @@ -45,9 +45,11 @@ "tinycon": "^0.6.8", "tss-react": "^4.9.10", "use-local-storage": "^3.0.0", + "vite-plugin-biome": "^1.0.10", "websocket-heartbeat-js": "^1.1.3" }, "devDependencies": { + "@biomejs/biome": "^1.8.1", "@lingui/cli": "^4.11.1", "@lingui/vite-plugin": "^4.11.1", "@types/mousetrap": "^1.6.15", @@ -58,35 +60,16 @@ "@types/swagger-ui-react": "^4.18.3", "@types/throttle-debounce": "^5.0.2", "@types/tinycon": "^0.6.5", - "@typescript-eslint/eslint-plugin": "^7.13.0", "@vitejs/plugin-react": "^4.3.1", "babel-plugin-macros": "^3.1.0", - "eslint": "^8.57.0", - "eslint-config-love": "^47.0.0", - "eslint-config-prettier": "^9.1.0", - "eslint-config-standard": "^17.1.0", - "eslint-plugin-prettier": "^5.1.3", - "eslint-plugin-react": "^7.34.2", - "eslint-plugin-react-hooks": "^4.6.2", - "prettier": "^3.3.2", "rollup-plugin-visualizer": "^5.12.0", "typescript": "^5.4.5", "vite": "^5.2.13", - "vite-plugin-eslint": "^1.8.1", "vite-tsconfig-paths": "^4.3.2", "vitest": "^1.6.0", "vitest-mock-extended": "^1.3.1" } }, - "node_modules/@aashutoshrathi/word-wrap": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", - "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/@ampproject/remapping": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", @@ -499,6 +482,152 @@ "node": ">=6.9.0" } }, + "node_modules/@biomejs/biome": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-1.8.1.tgz", + "integrity": "sha512-fQXGfvq6DIXem12dGQCM2tNF+vsNHH1qs3C7WeOu75Pd0trduoTmoO7G4ntLJ2qDs5wuw981H+cxQhi1uHnAtA==", + "hasInstallScript": true, + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" + }, + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "1.8.1", + "@biomejs/cli-darwin-x64": "1.8.1", + "@biomejs/cli-linux-arm64": "1.8.1", + "@biomejs/cli-linux-arm64-musl": "1.8.1", + "@biomejs/cli-linux-x64": "1.8.1", + "@biomejs/cli-linux-x64-musl": "1.8.1", + "@biomejs/cli-win32-arm64": "1.8.1", + "@biomejs/cli-win32-x64": "1.8.1" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-1.8.1.tgz", + "integrity": "sha512-XLiB7Uu6GALIOBWzQ2aMD0ru4Ly5/qSeQF7kk3AabzJ/kwsEWSe33iVySBP/SS2qv25cgqNiLksjGcw2bHT3mw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-darwin-x64": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-1.8.1.tgz", + "integrity": "sha512-uMTSxVLMfqkBVqyc25hSn83jBbp+wtWjzM/pHFlKXt3htJuw7FErVGW0nmQ9Sxa9vJ7GcqoltLMl28VQRIMYzg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-1.8.1.tgz", + "integrity": "sha512-3SzZRuC/9Oi2P2IBNPsEj0KXxSXUEYRR2kfRF/Ve8QAfGgrt4qnwuWd6QQKKN5R+oYH691qjm+cXBKEcrP1v/Q==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.8.1.tgz", + "integrity": "sha512-UQ8Wc01J0wQL+5AYOc7qkJn20B4PZmQL1KrmDZh7ot0DvD6aX4+8mmfd/dG5b6Zjo/44QvCKcvkFGCMRYuhWZA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-1.8.1.tgz", + "integrity": "sha512-AeBycVdNrTzsyYKEOtR2R0Ph0hCD0sCshcp2aOnfGP0hCZbtFg09D0SdKLbyzKntisY41HxKVrydYiaApp+2uw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.8.1.tgz", + "integrity": "sha512-fYbP/kNu/rtZ4kKzWVocIdqZOtBSUEg9qUhZaao3dy3CRzafR6u6KDtBeSCnt47O+iLnks1eOR1TUxzr5+QuqA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-arm64": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-1.8.1.tgz", + "integrity": "sha512-6tEd1H/iFKpgpE3OIB7oNgW5XkjiVMzMRPL8zYoZ036YfuJ5nMYm9eB9H/y81+8Z76vL48fiYzMPotJwukGPqQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-1.8.1.tgz", + "integrity": "sha512-g2H31jJzYmS4jkvl6TiyEjEX+Nv79a5km/xn+5DARTp5MBFzC9gwceusSSB2AkJKqZzY131AiACAWjKrVt5Ijw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, "node_modules/@cfaester/enzyme-adapter-react-18": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/@cfaester/enzyme-adapter-react-18/-/enzyme-adapter-react-18-0.8.0.tgz", @@ -1012,89 +1141,6 @@ "node": ">=12" } }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", - "dev": true, - "dependencies": { - "eslint-visitor-keys": "^3.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", - "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", - "dev": true, - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", - "dev": true, - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@eslint/eslintrc/node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@eslint/js": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", - "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, "node_modules/@exodus/schemasafe": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@exodus/schemasafe/-/schemasafe-1.3.0.tgz", @@ -1153,39 +1199,6 @@ "resolved": "https://registry.npmjs.org/@fontsource/open-sans/-/open-sans-5.0.28.tgz", "integrity": "sha512-hBvJHY76pJT/JynGUB5EXWhnzjYfLdcMn655J5p1v9lTT9HdQSy+keq2KPVXO2Htlg998BBa3p6u/jlrZ6w0kg==" }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.11.14", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", - "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", - "dev": true, - "dependencies": { - "@humanwhocodes/object-schema": "^2.0.2", - "debug": "^4.3.1", - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=10.10.0" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", - "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", - "dev": true - }, "node_modules/@jest/schemas": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", @@ -1595,53 +1608,6 @@ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@pkgr/core": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.0.tgz", - "integrity": "sha512-Zwq5OCzuwJC2jwqmpEQt7Ds1DTi6BWSwoGkbb1n9pO3hzb35BoJELx7c0T23iDkBGkh2e7tvOtjF3tr3OaQHDQ==", - "dev": true, - "engines": { - "node": "^12.20.0 || ^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/unts" - } - }, "node_modules/@redocly/ajv": { "version": "8.11.0", "resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.11.0.tgz", @@ -1738,19 +1704,6 @@ "node": ">=14.0.0" } }, - "node_modules/@rollup/pluginutils": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz", - "integrity": "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==", - "dev": true, - "dependencies": { - "estree-walker": "^2.0.1", - "picomatch": "^2.2.2" - }, - "engines": { - "node": ">= 8.0.0" - } - }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.13.2", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.13.2.tgz", @@ -1992,16 +1945,6 @@ "@babel/types": "^7.20.7" } }, - "node_modules/@types/eslint": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.40.0.tgz", - "integrity": "sha512-nbq2mvc/tBrK9zQQuItvjJl++GTN5j06DaPtp3hZCpngmG6Q3xoyEmd0TwZI0gAy/G1X0zhGBbr2imsGFdFV0g==", - "dev": true, - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", @@ -2034,13 +1977,6 @@ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" }, - "node_modules/@types/json5": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", - "dev": true, - "peer": true - }, "node_modules/@types/mousetrap": { "version": "1.6.15", "resolved": "https://registry.npmjs.org/@types/mousetrap/-/mousetrap-1.6.15.tgz", @@ -2145,506 +2081,6 @@ "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==" }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.13.0.tgz", - "integrity": "sha512-FX1X6AF0w8MdVFLSdqwqN/me2hyhuQg4ykN6ZpVhh1ij/80pTvDKclX1sZB9iqex8SjQfVhwMKs3JtnnMLzG9w==", - "dev": true, - "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "7.13.0", - "@typescript-eslint/type-utils": "7.13.0", - "@typescript-eslint/utils": "7.13.0", - "@typescript-eslint/visitor-keys": "7.13.0", - "graphemer": "^1.4.0", - "ignore": "^5.3.1", - "natural-compare": "^1.4.0", - "ts-api-utils": "^1.3.0" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^7.0.0", - "eslint": "^8.56.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/scope-manager": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.13.0.tgz", - "integrity": "sha512-ZrMCe1R6a01T94ilV13egvcnvVJ1pxShkE0+NDjDzH4nvG1wXpwsVI5bZCvE7AEDH1mXEx5tJSVR68bLgG7Dng==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "7.13.0", - "@typescript-eslint/visitor-keys": "7.13.0" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/types": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.13.0.tgz", - "integrity": "sha512-QWuwm9wcGMAuTsxP+qz6LBBd3Uq8I5Nv8xb0mk54jmNoCyDspnMvVsOxI6IsMmway5d1S9Su2+sCKv1st2l6eA==", - "dev": true, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.13.0.tgz", - "integrity": "sha512-nxn+dozQx+MK61nn/JP+M4eCkHDSxSLDpgE3WcQo0+fkjEolnaB5jswvIKC4K56By8MMgIho7f1PVxERHEo8rw==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "7.13.0", - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.0.2.tgz", - "integrity": "sha512-GdwfDglCxSmU+QTS9vhz2Sop46ebNCXpPPvsByK7hu0rFGRHL+AusKQJ7SoN+LbLh6APFpQwHKmDSwN35Z700Q==", - "dev": true, - "dependencies": { - "@typescript-eslint/scope-manager": "7.0.2", - "@typescript-eslint/types": "7.0.2", - "@typescript-eslint/typescript-estree": "7.0.2", - "@typescript-eslint/visitor-keys": "7.0.2", - "debug": "^4.3.4" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.56.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.0.2.tgz", - "integrity": "sha512-l6sa2jF3h+qgN2qUMjVR3uCNGjWw4ahGfzIYsCtFrQJCjhbrDPdiihYT8FnnqFwsWX+20hK592yX9I2rxKTP4g==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "7.0.2", - "@typescript-eslint/visitor-keys": "7.0.2" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.13.0.tgz", - "integrity": "sha512-xMEtMzxq9eRkZy48XuxlBFzpVMDurUAfDu5Rz16GouAtXm0TaAoTFzqWUFPPuQYXI/CDaH/Bgx/fk/84t/Bc9A==", - "dev": true, - "dependencies": { - "@typescript-eslint/typescript-estree": "7.13.0", - "@typescript-eslint/utils": "7.13.0", - "debug": "^4.3.4", - "ts-api-utils": "^1.3.0" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.56.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.13.0.tgz", - "integrity": "sha512-QWuwm9wcGMAuTsxP+qz6LBBd3Uq8I5Nv8xb0mk54jmNoCyDspnMvVsOxI6IsMmway5d1S9Su2+sCKv1st2l6eA==", - "dev": true, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.13.0.tgz", - "integrity": "sha512-cAvBvUoobaoIcoqox1YatXOnSl3gx92rCZoMRPzMNisDiM12siGilSM4+dJAekuuHTibI2hVC2fYK79iSFvWjw==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "7.13.0", - "@typescript-eslint/visitor-keys": "7.13.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^1.3.0" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/visitor-keys": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.13.0.tgz", - "integrity": "sha512-nxn+dozQx+MK61nn/JP+M4eCkHDSxSLDpgE3WcQo0+fkjEolnaB5jswvIKC4K56By8MMgIho7f1PVxERHEo8rw==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "7.13.0", - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@typescript-eslint/type-utils/node_modules/minimatch": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", - "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@typescript-eslint/type-utils/node_modules/semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.0.2.tgz", - "integrity": "sha512-ZzcCQHj4JaXFjdOql6adYV4B/oFOFjPOC9XYwCaZFRvqN8Llfvv4gSxrkQkd2u4Ci62i2c6W6gkDwQJDaRc4nA==", - "dev": true, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.0.2.tgz", - "integrity": "sha512-3AMc8khTcELFWcKcPc0xiLviEvvfzATpdPj/DXuOGIdQIIFybf4DMT1vKRbuAEOFMwhWt7NFLXRkbjsvKZQyvw==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "7.0.2", - "@typescript-eslint/visitor-keys": "7.0.2", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "minimatch": "9.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, - "node_modules/@typescript-eslint/utils": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.13.0.tgz", - "integrity": "sha512-jceD8RgdKORVnB4Y6BqasfIkFhl4pajB1wVxrF4akxD2QPM8GNYjgGwEzYS+437ewlqqrg7Dw+6dhdpjMpeBFQ==", - "dev": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "7.13.0", - "@typescript-eslint/types": "7.13.0", - "@typescript-eslint/typescript-estree": "7.13.0" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.56.0" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/scope-manager": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.13.0.tgz", - "integrity": "sha512-ZrMCe1R6a01T94ilV13egvcnvVJ1pxShkE0+NDjDzH4nvG1wXpwsVI5bZCvE7AEDH1mXEx5tJSVR68bLgG7Dng==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "7.13.0", - "@typescript-eslint/visitor-keys": "7.13.0" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/types": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.13.0.tgz", - "integrity": "sha512-QWuwm9wcGMAuTsxP+qz6LBBd3Uq8I5Nv8xb0mk54jmNoCyDspnMvVsOxI6IsMmway5d1S9Su2+sCKv1st2l6eA==", - "dev": true, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.13.0.tgz", - "integrity": "sha512-cAvBvUoobaoIcoqox1YatXOnSl3gx92rCZoMRPzMNisDiM12siGilSM4+dJAekuuHTibI2hVC2fYK79iSFvWjw==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "7.13.0", - "@typescript-eslint/visitor-keys": "7.13.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^1.3.0" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/visitor-keys": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.13.0.tgz", - "integrity": "sha512-nxn+dozQx+MK61nn/JP+M4eCkHDSxSLDpgE3WcQo0+fkjEolnaB5jswvIKC4K56By8MMgIho7f1PVxERHEo8rw==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "7.13.0", - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/minimatch": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", - "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.0.2.tgz", - "integrity": "sha512-8Y+YiBmqPighbm5xA2k4wKTxRzx9EkBu7Rlw+WHqMvRJ3RPz/BMBO9b2ru0LUNmXg120PHUXD5+SWFy2R8DqlQ==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "7.0.2", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", - "dev": true - }, "node_modules/@vitejs/plugin-react": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.1.tgz", @@ -2781,15 +2217,6 @@ "node": ">=0.4.0" } }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, "node_modules/acorn-walk": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", @@ -2799,22 +2226,6 @@ "node": ">=0.4.0" } }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -2885,35 +2296,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array-includes": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", - "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.4", - "is-string": "^1.0.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/array.prototype.filter": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/array.prototype.filter/-/array.prototype.filter-1.0.3.tgz", @@ -2933,50 +2315,11 @@ "url": "https://github.com/sponsors/ljharb" } }, - "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, - "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.findlastindex": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.4.tgz", - "integrity": "sha512-hzvSHUshSpCflDR1QMUBLHGHP1VIEBegT4pix9H/Z92Xw3ySoy6c2qh7lJWTJnRJ8JCZ9bJNCgTyYaJGcJu6xQ==", - "dev": true, - "peer": true, - "dependencies": { - "call-bind": "^1.0.5", - "define-properties": "^1.2.1", - "es-abstract": "^1.22.3", - "es-errors": "^1.3.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.2", "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", + "peer": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.2.0", @@ -2990,49 +2333,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array.prototype.flatmap": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", - "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.toreversed": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/array.prototype.toreversed/-/array.prototype.toreversed-1.1.2.tgz", - "integrity": "sha512-wwDCoT4Ck4Cz7sLtgUmzR5UV3YF5mFHUlbChCzZBQZ+0m2cl/DH3tKgvphv1nKgFsJ48oCSg6p91q2Vm0I/ZMA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" - } - }, - "node_modules/array.prototype.tosorted": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.3.tgz", - "integrity": "sha512-/DdH4TiTmOKzyQbp/eadcCVexiCb36xJg7HshYOYJnNZFDj33GEv0P7GxsynpShhq4OLYJzbGcBDkLsDt7MnNg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.5", - "define-properties": "^1.2.1", - "es-abstract": "^1.22.3", - "es-errors": "^1.1.0", - "es-shim-unscopables": "^1.0.2" - } - }, "node_modules/arraybuffer.prototype.slice": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", @@ -3250,65 +2550,6 @@ "ieee754": "^1.1.13" } }, - "node_modules/builtin-modules": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", - "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", - "dev": true, - "peer": true, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/builtins": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.0.1.tgz", - "integrity": "sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==", - "dev": true, - "peer": true, - "dependencies": { - "semver": "^7.0.0" - } - }, - "node_modules/builtins/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "peer": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/builtins/node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "dev": true, - "peer": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/builtins/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "peer": true - }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -3839,12 +3080,6 @@ "node": ">=6" } }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true - }, "node_modules/defaults": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", @@ -3920,36 +3155,12 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/discontinuous-range": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz", "integrity": "sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ==", "peer": true }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/dom-helpers": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", @@ -4174,31 +3385,6 @@ "node": ">= 0.4" } }, - "node_modules/es-iterator-helpers": { - "version": "1.0.19", - "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.19.tgz", - "integrity": "sha512-zoMwbCcH5hwUkKJkT8kDIBZSz9I6mVG//+lDCinLCGov4+r7NIy0ld8o03M0cJxl2spVf6ESYVS6/gpIfq1FFw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.3", - "es-errors": "^1.3.0", - "es-set-tostringtag": "^2.0.3", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "globalthis": "^1.0.3", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.0.3", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.7", - "iterator.prototype": "^1.1.2", - "safe-array-concat": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/es-object-atoms": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", @@ -4227,6 +3413,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", + "peer": true, "dependencies": { "hasown": "^2.0.0" } @@ -4313,612 +3500,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", - "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", - "dev": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.0", - "@humanwhocodes/config-array": "^0.11.14", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-compat-utils": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/eslint-compat-utils/-/eslint-compat-utils-0.1.2.tgz", - "integrity": "sha512-Jia4JDldWnFNIru1Ehx1H5s9/yxiRHY/TimCuUc0jNexew3cF1gI6CYZil1ociakfWO3rRqFjl1mskBblB3RYg==", - "dev": true, - "peer": true, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "eslint": ">=6.0.0" - } - }, - "node_modules/eslint-config-love": { - "version": "47.0.0", - "resolved": "https://registry.npmjs.org/eslint-config-love/-/eslint-config-love-47.0.0.tgz", - "integrity": "sha512-wIeJhb4/NF7nE5Ltppg1e9dp1Auxx0+ZPRysrXQ3uBKlW4Nj/UiTZu4r3sKWCxo6HGcRcI4MC1Q5421y3fny2w==", - "dev": true, - "dependencies": { - "@typescript-eslint/parser": "^7.0.1" - }, - "peerDependencies": { - "@typescript-eslint/eslint-plugin": "^7.0.1", - "eslint": "^8.0.1", - "eslint-plugin-import": "^2.25.2", - "eslint-plugin-n": "^15.0.0 || ^16.0.0 ", - "eslint-plugin-promise": "^6.0.0", - "typescript": "*" - } - }, - "node_modules/eslint-config-prettier": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", - "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", - "dev": true, - "bin": { - "eslint-config-prettier": "bin/cli.js" - }, - "peerDependencies": { - "eslint": ">=7.0.0" - } - }, - "node_modules/eslint-config-standard": { - "version": "17.1.0", - "resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-17.1.0.tgz", - "integrity": "sha512-IwHwmaBNtDK4zDHQukFDW5u/aTb8+meQWZvNFWkiGmbWjD6bqyuSSBxxXKkCftCUzc1zwCH2m/baCNDLGmuO5Q==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "eslint": "^8.0.1", - "eslint-plugin-import": "^2.25.2", - "eslint-plugin-n": "^15.0.0 || ^16.0.0 ", - "eslint-plugin-promise": "^6.0.0" - } - }, - "node_modules/eslint-import-resolver-node": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", - "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", - "dev": true, - "peer": true, - "dependencies": { - "debug": "^3.2.7", - "is-core-module": "^2.13.0", - "resolve": "^1.22.4" - } - }, - "node_modules/eslint-import-resolver-node/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "peer": true, - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-module-utils": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.1.tgz", - "integrity": "sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q==", - "dev": true, - "peer": true, - "dependencies": { - "debug": "^3.2.7" - }, - "engines": { - "node": ">=4" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - } - } - }, - "node_modules/eslint-module-utils/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "peer": true, - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-plugin-es-x": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-es-x/-/eslint-plugin-es-x-7.5.0.tgz", - "integrity": "sha512-ODswlDSO0HJDzXU0XvgZ3lF3lS3XAZEossh15Q2UHjwrJggWeBoKqqEsLTZLXl+dh5eOAozG0zRcYtuE35oTuQ==", - "dev": true, - "peer": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.1.2", - "@eslint-community/regexpp": "^4.6.0", - "eslint-compat-utils": "^0.1.2" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ota-meshi" - }, - "peerDependencies": { - "eslint": ">=8" - } - }, - "node_modules/eslint-plugin-import": { - "version": "2.29.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz", - "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==", - "dev": true, - "peer": true, - "dependencies": { - "array-includes": "^3.1.7", - "array.prototype.findlastindex": "^1.2.3", - "array.prototype.flat": "^1.3.2", - "array.prototype.flatmap": "^1.3.2", - "debug": "^3.2.7", - "doctrine": "^2.1.0", - "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.8.0", - "hasown": "^2.0.0", - "is-core-module": "^2.13.1", - "is-glob": "^4.0.3", - "minimatch": "^3.1.2", - "object.fromentries": "^2.0.7", - "object.groupby": "^1.0.1", - "object.values": "^1.1.7", - "semver": "^6.3.1", - "tsconfig-paths": "^3.15.0" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" - } - }, - "node_modules/eslint-plugin-import/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "peer": true, - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-plugin-import/node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "peer": true, - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eslint-plugin-n": { - "version": "16.6.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-16.6.2.tgz", - "integrity": "sha512-6TyDmZ1HXoFQXnhCTUjVFULReoBPOAjpuiKELMkeP40yffI/1ZRO+d9ug/VC6fqISo2WkuIBk3cvuRPALaWlOQ==", - "dev": true, - "peer": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "builtins": "^5.0.1", - "eslint-plugin-es-x": "^7.5.0", - "get-tsconfig": "^4.7.0", - "globals": "^13.24.0", - "ignore": "^5.2.4", - "is-builtin-module": "^3.2.1", - "is-core-module": "^2.12.1", - "minimatch": "^3.1.2", - "resolve": "^1.22.2", - "semver": "^7.5.3" - }, - "engines": { - "node": ">=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - }, - "peerDependencies": { - "eslint": ">=7.0.0" - } - }, - "node_modules/eslint-plugin-n/node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "peer": true, - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint-plugin-n/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "peer": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/eslint-plugin-n/node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "dev": true, - "peer": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/eslint-plugin-n/node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "peer": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint-plugin-n/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "peer": true - }, - "node_modules/eslint-plugin-prettier": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz", - "integrity": "sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==", - "dev": true, - "dependencies": { - "prettier-linter-helpers": "^1.0.0", - "synckit": "^0.8.6" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint-plugin-prettier" - }, - "peerDependencies": { - "@types/eslint": ">=8.0.0", - "eslint": ">=8.0.0", - "eslint-config-prettier": "*", - "prettier": ">=3.0.0" - }, - "peerDependenciesMeta": { - "@types/eslint": { - "optional": true - }, - "eslint-config-prettier": { - "optional": true - } - } - }, - "node_modules/eslint-plugin-promise": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.1.1.tgz", - "integrity": "sha512-tjqWDwVZQo7UIPMeDReOpUgHCmCiH+ePnVT+5zVapL0uuHnegBUs2smM13CzOs2Xb5+MHMRFTs9v24yjba4Oig==", - "dev": true, - "peer": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - } - }, - "node_modules/eslint-plugin-react": { - "version": "7.34.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.34.2.tgz", - "integrity": "sha512-2HCmrU+/JNigDN6tg55cRDKCQWicYAPB38JGSFDQt95jDm8rrvSUo7YPkOIm5l6ts1j1zCvysNcasvfTMQzUOw==", - "dev": true, - "dependencies": { - "array-includes": "^3.1.8", - "array.prototype.findlast": "^1.2.5", - "array.prototype.flatmap": "^1.3.2", - "array.prototype.toreversed": "^1.1.2", - "array.prototype.tosorted": "^1.1.3", - "doctrine": "^2.1.0", - "es-iterator-helpers": "^1.0.19", - "estraverse": "^5.3.0", - "jsx-ast-utils": "^2.4.1 || ^3.0.0", - "minimatch": "^3.1.2", - "object.entries": "^1.1.8", - "object.fromentries": "^2.0.8", - "object.hasown": "^1.1.4", - "object.values": "^1.2.0", - "prop-types": "^15.8.1", - "resolve": "^2.0.0-next.5", - "semver": "^6.3.1", - "string.prototype.matchall": "^4.0.11" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" - } - }, - "node_modules/eslint-plugin-react-hooks": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", - "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", - "dev": true, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" - } - }, - "node_modules/eslint-plugin-react/node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eslint-plugin-react/node_modules/resolve": { - "version": "2.0.0-next.5", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", - "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", - "dev": true, - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", - "dev": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint/node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/eslint/node_modules/globals": { - "version": "13.20.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", - "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", - "dev": true, - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint/node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", - "dev": true, - "dependencies": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", - "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", - "dev": true, - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", - "dev": true - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/eventemitter3": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", @@ -5005,67 +3586,11 @@ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, - "node_modules/fast-diff": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", - "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", - "dev": true - }, - "node_modules/fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", - "dev": true, - "dependencies": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true - }, "node_modules/fast-safe-stringify": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==" }, - "node_modules/fastq": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", - "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", - "dev": true, - "dependencies": { - "reusify": "^1.0.4" - } - }, "node_modules/figures": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", @@ -5090,18 +3615,6 @@ "node": ">=0.8.0" } }, - "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", - "dev": true, - "dependencies": { - "flat-cache": "^3.0.4" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -5119,41 +3632,6 @@ "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", - "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", - "dev": true, - "dependencies": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/flatted": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", - "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", - "dev": true - }, "node_modules/follow-redirects": { "version": "1.15.6", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", @@ -5332,19 +3810,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-tsconfig": { - "version": "4.7.2", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.2.tgz", - "integrity": "sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==", - "dev": true, - "peer": true, - "dependencies": { - "resolve-pkg-maps": "^1.0.0" - }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" - } - }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -5399,26 +3864,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/globrex": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", @@ -5436,12 +3881,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true - }, "node_modules/has": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/has/-/has-1.0.4.tgz", @@ -5610,15 +4049,6 @@ } ] }, - "node_modules/ignore": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", - "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, "node_modules/immer": { "version": "10.0.3", "resolved": "https://registry.npmjs.org/immer/-/immer-10.0.3.tgz", @@ -5643,15 +4073,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "engines": { - "node": ">=0.8.19" - } - }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -5748,21 +4169,6 @@ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" }, - "node_modules/is-async-function": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz", - "integrity": "sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==", - "dev": true, - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-bigint": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", @@ -5801,22 +4207,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-builtin-module": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", - "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==", - "dev": true, - "peer": true, - "dependencies": { - "builtin-modules": "^3.3.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-callable": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", @@ -5891,18 +4281,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-finalizationregistry": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz", - "integrity": "sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -5911,21 +4289,6 @@ "node": ">=8" } }, - "node_modules/is-generator-function": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", - "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", - "dev": true, - "dependencies": { - "has-tostringtag": "^1.0.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", @@ -5947,18 +4310,6 @@ "node": ">=8" } }, - "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, - "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", @@ -5993,15 +4344,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -6017,18 +4359,6 @@ "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, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-shared-array-buffer": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", @@ -6115,18 +4445,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "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==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-weakref": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", @@ -6138,22 +4456,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-weakset": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.3.tgz", - "integrity": "sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "get-intrinsic": "^1.2.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-wsl": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", @@ -6177,19 +4479,6 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, - "node_modules/iterator.prototype": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.2.tgz", - "integrity": "sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==", - "dev": true, - "dependencies": { - "define-properties": "^1.2.1", - "get-intrinsic": "^1.2.1", - "has-symbols": "^1.0.3", - "reflect.getprototypeof": "^1.0.4", - "set-function-name": "^2.0.1" - } - }, "node_modules/jest-get-type": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", @@ -6275,18 +4564,6 @@ "foreach": "^2.0.4" } }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true - }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -6305,21 +4582,6 @@ "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", "dev": true }, - "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, - "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/klona": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", @@ -6336,19 +4598,6 @@ "node": ">=6" } }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -6370,21 +4619,6 @@ "url": "https://github.com/sponsors/antfu" } }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", @@ -6413,12 +4647,6 @@ "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==" }, - "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 - }, "node_modules/lodash.sortby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", @@ -6506,15 +4734,6 @@ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", "dev": true }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, "node_modules/micromatch": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", @@ -6568,16 +4787,6 @@ "node": "*" } }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "peer": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/mlly": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.4.2.tgz", @@ -6691,12 +4900,6 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true - }, "node_modules/nearley": { "version": "2.20.1", "resolved": "https://registry.npmjs.org/nearley/-/nearley-2.20.1.tgz", @@ -6942,68 +5145,21 @@ "version": "1.1.8", "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.8.tgz", "integrity": "sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "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, - "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.groupby": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.2.tgz", - "integrity": "sha512-bzBq58S+x+uo0VjurFT0UktpKHOZmv4/xePiOA1nbB9pMqpGK7rUPNgf+1YC+7mE+0HzhTMqNUuCqvKhj6FnBw==", - "dev": true, "peer": true, "dependencies": { - "array.prototype.filter": "^1.0.3", - "call-bind": "^1.0.5", + "call-bind": "^1.0.7", "define-properties": "^1.2.1", - "es-abstract": "^1.22.3", - "es-errors": "^1.0.0" - } - }, - "node_modules/object.hasown": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.4.tgz", - "integrity": "sha512-FZ9LZt9/RHzGySlBARE3VF+gE26TxR38SdmqOqliuTnl9wrKulaQs+4dee1V+Io8VfxqzAfHu6YuRgUy8OHoTg==", - "dev": true, - "dependencies": { - "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.0", "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==", + "peer": true, "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -7066,23 +5222,6 @@ "json-pointer": "0.6.2" } }, - "node_modules/optionator": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", - "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", - "dev": true, - "dependencies": { - "@aashutoshrathi/word-wrap": "^1.2.3", - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/ora": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", @@ -7115,36 +5254,6 @@ "node": ">=0.10.0" } }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -7212,15 +5321,6 @@ "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==" }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -7446,43 +5546,6 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "peer": true }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prettier": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.2.tgz", - "integrity": "sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==", - "dev": true, - "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/prettier-linter-helpers": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", - "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", - "dev": true, - "dependencies": { - "fast-diff": "^1.1.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/pretty-format": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", @@ -7558,26 +5621,6 @@ "node": ">=6" } }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, "node_modules/raf": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", @@ -8038,27 +6081,6 @@ "redux": "^5.0.0" } }, - "node_modules/reflect.getprototypeof": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz", - "integrity": "sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.1", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "globalthis": "^1.0.3", - "which-builtin-type": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/reftools": { "version": "1.1.9", "resolved": "https://registry.npmjs.org/reftools/-/reftools-1.1.9.tgz", @@ -8134,16 +6156,6 @@ "node": ">=4" } }, - "node_modules/resolve-pkg-maps": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "dev": true, - "peer": true, - "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" - } - }, "node_modules/restore-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", @@ -8166,31 +6178,6 @@ "node": ">=0.12" } }, - "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/rollup": { "version": "4.13.2", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.13.2.tgz", @@ -8279,29 +6266,6 @@ "node": ">=0.12.0" } }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, "node_modules/rxjs": { "version": "6.6.7", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", @@ -8530,15 +6494,6 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/slugify": { "version": "1.4.7", "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.4.7.tgz", @@ -8612,32 +6567,6 @@ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, - "node_modules/string.prototype.matchall": { - "version": "4.0.11", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz", - "integrity": "sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg==", - "dev": true, - "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", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.7", - "regexp.prototype.flags": "^1.5.2", - "set-function-name": "^2.0.2", - "side-channel": "^1.0.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/string.prototype.trim": { "version": "1.2.9", "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", @@ -8695,16 +6624,6 @@ "node": ">=8" } }, - "node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true, - "peer": true, - "engines": { - "node": ">=4" - } - }, "node_modules/strip-final-newline": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", @@ -8717,18 +6636,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "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, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/strip-literal": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.0.0.tgz", @@ -8874,33 +6781,11 @@ "url": "https://github.com/Mermade/oas-kit?sponsor=1" } }, - "node_modules/synckit": { - "version": "0.8.8", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.8.tgz", - "integrity": "sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==", - "dev": true, - "dependencies": { - "@pkgr/core": "^0.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/unts" - } - }, "node_modules/tabbable": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==" }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true - }, "node_modules/throttle-debounce": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.0.tgz", @@ -8981,18 +6866,6 @@ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, - "node_modules/ts-api-utils": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", - "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", - "dev": true, - "engines": { - "node": ">=16" - }, - "peerDependencies": { - "typescript": ">=4.2.0" - } - }, "node_modules/ts-essentials": { "version": "9.3.2", "resolved": "https://registry.npmjs.org/ts-essentials/-/ts-essentials-9.3.2.tgz", @@ -9022,32 +6895,6 @@ } } }, - "node_modules/tsconfig-paths": { - "version": "3.15.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", - "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", - "dev": true, - "peer": true, - "dependencies": { - "@types/json5": "^0.0.29", - "json5": "^1.0.2", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" - } - }, - "node_modules/tsconfig-paths/node_modules/json5": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", - "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", - "dev": true, - "peer": true, - "dependencies": { - "minimist": "^1.2.0" - }, - "bin": { - "json5": "lib/cli.js" - } - }, "node_modules/tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", @@ -9077,18 +6924,6 @@ } } }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -9459,34 +7294,12 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/vite-plugin-eslint": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/vite-plugin-eslint/-/vite-plugin-eslint-1.8.1.tgz", - "integrity": "sha512-PqdMf3Y2fLO9FsNPmMX+//2BF5SF8nEWspZdgl4kSt7UvHDRHVVfHvxsD7ULYzZrJDGRxR81Nq7TOFgwMnUang==", - "dev": true, - "dependencies": { - "@rollup/pluginutils": "^4.2.1", - "@types/eslint": "^8.4.5", - "rollup": "^2.77.2" - }, + "node_modules/vite-plugin-biome": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/vite-plugin-biome/-/vite-plugin-biome-1.0.10.tgz", + "integrity": "sha512-jUmAr0hmls8g6PQc7Jy7GKGhHA2dxWcZ3p4+hXi9RPKV3lIrYfJi1W+F36N5gUu3no8QlEkUOEgUdbiNVnDvUw==", "peerDependencies": { - "eslint": ">=7", - "vite": ">=2" - } - }, - "node_modules/vite-plugin-eslint/node_modules/rollup": { - "version": "2.79.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz", - "integrity": "sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==", - "dev": true, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=10.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" + "@biomejs/biome": "^1.4.1" } }, "node_modules/vite-tsconfig-paths": { @@ -10034,50 +7847,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/which-builtin-type": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.3.tgz", - "integrity": "sha512-YmjsSMDBYsM1CaFiayOVT06+KJeXf0o5M/CAd4o1lTadFAtacTUM49zoYxr/oroopFDfhvN6iEcBxUyc3gvKmw==", - "dev": true, - "dependencies": { - "function.prototype.name": "^1.1.5", - "has-tostringtag": "^1.0.0", - "is-async-function": "^2.0.0", - "is-date-object": "^1.0.5", - "is-finalizationregistry": "^1.0.2", - "is-generator-function": "^1.0.10", - "is-regex": "^1.1.4", - "is-weakref": "^1.0.2", - "isarray": "^2.0.5", - "which-boxed-primitive": "^1.0.2", - "which-collection": "^1.0.1", - "which-typed-array": "^1.1.9" - }, - "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, - "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.15", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", @@ -10185,18 +7954,6 @@ "engines": { "node": ">=12" } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } } } } diff --git a/commafeed-client/package.json b/commafeed-client/package.json index 69bf5a7c..32195248 100644 --- a/commafeed-client/package.json +++ b/commafeed-client/package.json @@ -1,86 +1,79 @@ { - "name": "commafeed-client", - "private": true, - "version": "0.0.0", - "type": "module", - "scripts": { - "dev": "vite --host", - "dev:typescript": "tsc --watch", - "build": "tsc && vite build", - "preview": "vite preview", - "test": "vitest", - "test:ci": "vitest run", - "eslint": "eslint --ext=.js,.jsx,.ts,.tsx src", - "i18n:extract": "lingui extract --clean" - }, - "dependencies": { - "@emotion/react": "^11.11.4", - "@fontsource/open-sans": "^5.0.28", - "@lingui/core": "^4.11.1", - "@lingui/macro": "^4.11.1", - "@lingui/react": "^4.11.1", - "@mantine/core": "^7.10.1", - "@mantine/form": "^7.10.1", - "@mantine/hooks": "^7.10.1", - "@mantine/modals": "^7.10.1", - "@mantine/notifications": "^7.10.1", - "@mantine/spotlight": "^7.10.1", - "@monaco-editor/react": "^4.6.0", - "@reduxjs/toolkit": "^2.2.5", - "axios": "^1.7.2", - "dayjs": "^1.11.11", - "escape-string-regexp": "^5.0.0", - "interweave": "^13.1.0", - "monaco-editor": "^0.49.0", - "mousetrap": "^1.6.5", - "react": "^18.3.1", - "react-async-hook": "^4.0.0", - "react-contexify": "^6.0.0", - "react-device-detect": "^2.2.3", - "react-dom": "^18.3.1", - "react-draggable": "^4.4.6", - "react-ga4": "^2.1.0", - "react-helmet": "^6.1.0", - "react-icons": "^5.2.1", - "react-infinite-scroller": "^1.2.6", - "react-redux": "^9.1.2", - "react-router-dom": "^6.23.1", - "react-swipeable": "^7.0.1", - "redoc": "^2.1.5", - "throttle-debounce": "^5.0.0", - "tinycon": "^0.6.8", - "tss-react": "^4.9.10", - "use-local-storage": "^3.0.0", - "websocket-heartbeat-js": "^1.1.3" - }, - "devDependencies": { - "@lingui/cli": "^4.11.1", - "@lingui/vite-plugin": "^4.11.1", - "@types/mousetrap": "^1.6.15", - "@types/react": "^18.3.3", - "@types/react-dom": "^18.3.0", - "@types/react-helmet": "^6.1.11", - "@types/react-infinite-scroller": "^1.2.5", - "@types/swagger-ui-react": "^4.18.3", - "@types/throttle-debounce": "^5.0.2", - "@types/tinycon": "^0.6.5", - "@typescript-eslint/eslint-plugin": "^7.13.0", - "@vitejs/plugin-react": "^4.3.1", - "babel-plugin-macros": "^3.1.0", - "eslint": "^8.57.0", - "eslint-config-love": "^47.0.0", - "eslint-config-prettier": "^9.1.0", - "eslint-config-standard": "^17.1.0", - "eslint-plugin-prettier": "^5.1.3", - "eslint-plugin-react": "^7.34.2", - "eslint-plugin-react-hooks": "^4.6.2", - "prettier": "^3.3.2", - "rollup-plugin-visualizer": "^5.12.0", - "typescript": "^5.4.5", - "vite": "^5.2.13", - "vite-plugin-eslint": "^1.8.1", - "vite-tsconfig-paths": "^4.3.2", - "vitest": "^1.6.0", - "vitest-mock-extended": "^1.3.1" - } + "name": "commafeed-client", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite --host", + "dev:typescript": "tsc --watch", + "build": "tsc && vite build", + "preview": "vite preview", + "test": "vitest", + "test:ci": "vitest run", + "lint": "biome check ./src", + "lint:fix": "biome check --write ./src", + "i18n:extract": "lingui extract --clean" + }, + "dependencies": { + "@emotion/react": "^11.11.4", + "@fontsource/open-sans": "^5.0.28", + "@lingui/core": "^4.11.1", + "@lingui/macro": "^4.11.1", + "@lingui/react": "^4.11.1", + "@mantine/core": "^7.10.1", + "@mantine/form": "^7.10.1", + "@mantine/hooks": "^7.10.1", + "@mantine/modals": "^7.10.1", + "@mantine/notifications": "^7.10.1", + "@mantine/spotlight": "^7.10.1", + "@monaco-editor/react": "^4.6.0", + "@reduxjs/toolkit": "^2.2.5", + "axios": "^1.7.2", + "dayjs": "^1.11.11", + "escape-string-regexp": "^5.0.0", + "interweave": "^13.1.0", + "monaco-editor": "^0.49.0", + "mousetrap": "^1.6.5", + "react": "^18.3.1", + "react-async-hook": "^4.0.0", + "react-contexify": "^6.0.0", + "react-device-detect": "^2.2.3", + "react-dom": "^18.3.1", + "react-draggable": "^4.4.6", + "react-ga4": "^2.1.0", + "react-helmet": "^6.1.0", + "react-icons": "^5.2.1", + "react-infinite-scroller": "^1.2.6", + "react-redux": "^9.1.2", + "react-router-dom": "^6.23.1", + "react-swipeable": "^7.0.1", + "redoc": "^2.1.5", + "throttle-debounce": "^5.0.0", + "tinycon": "^0.6.8", + "tss-react": "^4.9.10", + "use-local-storage": "^3.0.0", + "vite-plugin-biome": "^1.0.10", + "websocket-heartbeat-js": "^1.1.3" + }, + "devDependencies": { + "@biomejs/biome": "^1.8.1", + "@lingui/cli": "^4.11.1", + "@lingui/vite-plugin": "^4.11.1", + "@types/mousetrap": "^1.6.15", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@types/react-helmet": "^6.1.11", + "@types/react-infinite-scroller": "^1.2.5", + "@types/swagger-ui-react": "^4.18.3", + "@types/throttle-debounce": "^5.0.2", + "@types/tinycon": "^0.6.5", + "@vitejs/plugin-react": "^4.3.1", + "babel-plugin-macros": "^3.1.0", + "rollup-plugin-visualizer": "^5.12.0", + "typescript": "^5.4.5", + "vite": "^5.2.13", + "vite-tsconfig-paths": "^4.3.2", + "vitest": "^1.6.0", + "vitest-mock-extended": "^1.3.1" + } } diff --git a/commafeed-client/src/App.tsx b/commafeed-client/src/App.tsx index d66f26f8..c8e97617 100644 --- a/commafeed-client/src/App.tsx +++ b/commafeed-client/src/App.tsx @@ -14,6 +14,7 @@ import { Header } from "components/header/Header" import { Tree } from "components/sidebar/Tree" import { useBrowserExtension } from "hooks/useBrowserExtension" import { useI18n } from "i18n" +import { WelcomePage } from "pages/WelcomePage" import { AdminUsersPage } from "pages/admin/AdminUsersPage" import { MetricsPage } from "pages/admin/MetricsPage" import { AboutPage } from "pages/app/AboutPage" @@ -28,7 +29,6 @@ import { TagDetailsPage } from "pages/app/TagDetailsPage" import { LoginPage } from "pages/auth/LoginPage" import { PasswordRecoveryPage } from "pages/auth/PasswordRecoveryPage" import { RegistrationPage } from "pages/auth/RegistrationPage" -import { WelcomePage } from "pages/WelcomePage" import React, { useEffect } from "react" import { isSafari } from "react-device-detect" import ReactGA from "react-ga4" diff --git a/commafeed-client/src/app/async-thunk.ts b/commafeed-client/src/app/async-thunk.ts index e22cf20a..b40bf5ea 100644 --- a/commafeed-client/src/app/async-thunk.ts +++ b/commafeed-client/src/app/async-thunk.ts @@ -1,7 +1,7 @@ -import { createAsyncThunk } from "@reduxjs/toolkit" -import { type AppDispatch, type RootState } from "app/store" - -export const createAppAsyncThunk = createAsyncThunk.withTypes<{ - state: RootState - dispatch: AppDispatch -}>() +import { createAsyncThunk } from "@reduxjs/toolkit" +import type { AppDispatch, RootState } from "app/store" + +export const createAppAsyncThunk = createAsyncThunk.withTypes<{ + state: RootState + dispatch: AppDispatch +}>() diff --git a/commafeed-client/src/app/client.ts b/commafeed-client/src/app/client.ts index 23b4d1cd..3e9a7cd1 100644 --- a/commafeed-client/src/app/client.ts +++ b/commafeed-client/src/app/client.ts @@ -1,127 +1,127 @@ -import axios, { type AxiosError } from "axios" -import { - type AddCategoryRequest, - type AdminSaveUserRequest, - type AuthenticationError, - type Category, - type CategoryModificationRequest, - type CollapseRequest, - type Entries, - type FeedInfo, - type FeedInfoRequest, - type FeedModificationRequest, - type GetEntriesPaginatedRequest, - type IDRequest, - type LoginRequest, - type MarkRequest, - type Metrics, - type MultipleMarkRequest, - type PasswordResetRequest, - type ProfileModificationRequest, - type RegistrationRequest, - type ServerInfo, - type Settings, - type StarRequest, - type SubscribeRequest, - type Subscription, - type TagRequest, - type UserModel, -} from "./types" - -const axiosInstance = axios.create({ baseURL: "./rest", withCredentials: true }) -axiosInstance.interceptors.response.use( - response => response, - error => { - if (isAuthenticationError(error)) { - const data = error.response?.data - window.location.hash = data?.allowRegistrations ? "/welcome" : "/login" - } - throw error - } -) - -function isAuthenticationError(error: unknown): error is AxiosError { - return axios.isAxiosError(error) && !!error.response && [401, 403].includes(error.response.status) -} - -export const client = { - category: { - getRoot: async () => await axiosInstance.get("category/get"), - modify: async (req: CategoryModificationRequest) => await axiosInstance.post("category/modify", req), - collapse: async (req: CollapseRequest) => await axiosInstance.post("category/collapse", req), - getEntries: async (req: GetEntriesPaginatedRequest) => await axiosInstance.get("category/entries", { params: req }), - markEntries: async (req: MarkRequest) => await axiosInstance.post("category/mark", req), - add: async (req: AddCategoryRequest) => await axiosInstance.post("category/add", req), - delete: async (req: IDRequest) => await axiosInstance.post("category/delete", req), - }, - entry: { - mark: async (req: MarkRequest) => await axiosInstance.post("entry/mark", req), - markMultiple: async (req: MultipleMarkRequest) => await axiosInstance.post("entry/markMultiple", req), - star: async (req: StarRequest) => await axiosInstance.post("entry/star", req), - getTags: async () => await axiosInstance.get("entry/tags"), - tag: async (req: TagRequest) => await axiosInstance.post("entry/tag", req), - }, - feed: { - get: async (id: string) => await axiosInstance.get(`feed/get/${id}`), - modify: async (req: FeedModificationRequest) => await axiosInstance.post("feed/modify", req), - getEntries: async (req: GetEntriesPaginatedRequest) => await axiosInstance.get("feed/entries", { params: req }), - markEntries: async (req: MarkRequest) => await axiosInstance.post("feed/mark", req), - fetchFeed: async (req: FeedInfoRequest) => await axiosInstance.post("feed/fetch", req), - refreshAll: async () => await axiosInstance.get("feed/refreshAll"), - subscribe: async (req: SubscribeRequest) => await axiosInstance.post("feed/subscribe", req), - unsubscribe: async (req: IDRequest) => await axiosInstance.post("feed/unsubscribe", req), - importOpml: async (req: File) => { - const formData = new FormData() - formData.append("file", req) - return await axiosInstance.post("feed/import", formData, { - headers: { - "Content-Type": "multipart/form-data", - }, - }) - }, - }, - user: { - login: async (req: LoginRequest) => await axiosInstance.post("user/login", req), - register: async (req: RegistrationRequest) => await axiosInstance.post("user/register", req), - passwordReset: async (req: PasswordResetRequest) => await axiosInstance.post("user/passwordReset", req), - getSettings: async () => await axiosInstance.get("user/settings"), - saveSettings: async (settings: Settings) => await axiosInstance.post("user/settings", settings), - getProfile: async () => await axiosInstance.get("user/profile"), - saveProfile: async (req: ProfileModificationRequest) => await axiosInstance.post("user/profile", req), - deleteProfile: async () => await axiosInstance.post("user/profile/deleteAccount"), - }, - server: { - getServerInfos: async () => await axiosInstance.get("server/get"), - }, - admin: { - getAllUsers: async () => await axiosInstance.get("admin/user/getAll"), - saveUser: async (req: AdminSaveUserRequest) => await axiosInstance.post("admin/user/save", req), - deleteUser: async (req: IDRequest) => await axiosInstance.post("admin/user/delete", req), - getMetrics: async () => await axiosInstance.get("admin/metrics"), - }, -} - -/** - * transform an error object to an array of strings that can be displayed to the user - * @param err an error object (e.g. from axios) - * @returns an array of messages to show the user - */ -export const errorToStrings = (err: unknown) => { - let strings: string[] = [] - - if (axios.isAxiosError(err) && err.response) { - if (typeof err.response.data === "string") strings.push(err.response.data) - if (isMessageError(err)) strings.push(err.response.data.message) - if (isMessageArrayError(err)) strings = [...strings, ...err.response.data.errors] - } - - return strings -} - -function isMessageError(err: AxiosError): err is AxiosError<{ message: string }> { - return !!err.response && !!err.response.data && typeof err.response.data === "object" && "message" in err.response.data -} - -function isMessageArrayError(err: AxiosError): err is AxiosError<{ errors: string[] }> { - return !!err.response && !!err.response.data && typeof err.response.data === "object" && "errors" in err.response.data -} +import axios, { type AxiosError } from "axios" +import type { + AddCategoryRequest, + AdminSaveUserRequest, + AuthenticationError, + Category, + CategoryModificationRequest, + CollapseRequest, + Entries, + FeedInfo, + FeedInfoRequest, + FeedModificationRequest, + GetEntriesPaginatedRequest, + IDRequest, + LoginRequest, + MarkRequest, + Metrics, + MultipleMarkRequest, + PasswordResetRequest, + ProfileModificationRequest, + RegistrationRequest, + ServerInfo, + Settings, + StarRequest, + SubscribeRequest, + Subscription, + TagRequest, + UserModel, +} from "./types" + +const axiosInstance = axios.create({ baseURL: "./rest", withCredentials: true }) +axiosInstance.interceptors.response.use( + response => response, + error => { + if (isAuthenticationError(error)) { + const data = error.response?.data + window.location.hash = data?.allowRegistrations ? "/welcome" : "/login" + } + throw error + } +) + +function isAuthenticationError(error: unknown): error is AxiosError { + return axios.isAxiosError(error) && !!error.response && [401, 403].includes(error.response.status) +} + +export const client = { + category: { + getRoot: async () => await axiosInstance.get("category/get"), + modify: async (req: CategoryModificationRequest) => await axiosInstance.post("category/modify", req), + collapse: async (req: CollapseRequest) => await axiosInstance.post("category/collapse", req), + getEntries: async (req: GetEntriesPaginatedRequest) => await axiosInstance.get("category/entries", { params: req }), + markEntries: async (req: MarkRequest) => await axiosInstance.post("category/mark", req), + add: async (req: AddCategoryRequest) => await axiosInstance.post("category/add", req), + delete: async (req: IDRequest) => await axiosInstance.post("category/delete", req), + }, + entry: { + mark: async (req: MarkRequest) => await axiosInstance.post("entry/mark", req), + markMultiple: async (req: MultipleMarkRequest) => await axiosInstance.post("entry/markMultiple", req), + star: async (req: StarRequest) => await axiosInstance.post("entry/star", req), + getTags: async () => await axiosInstance.get("entry/tags"), + tag: async (req: TagRequest) => await axiosInstance.post("entry/tag", req), + }, + feed: { + get: async (id: string) => await axiosInstance.get(`feed/get/${id}`), + modify: async (req: FeedModificationRequest) => await axiosInstance.post("feed/modify", req), + getEntries: async (req: GetEntriesPaginatedRequest) => await axiosInstance.get("feed/entries", { params: req }), + markEntries: async (req: MarkRequest) => await axiosInstance.post("feed/mark", req), + fetchFeed: async (req: FeedInfoRequest) => await axiosInstance.post("feed/fetch", req), + refreshAll: async () => await axiosInstance.get("feed/refreshAll"), + subscribe: async (req: SubscribeRequest) => await axiosInstance.post("feed/subscribe", req), + unsubscribe: async (req: IDRequest) => await axiosInstance.post("feed/unsubscribe", req), + importOpml: async (req: File) => { + const formData = new FormData() + formData.append("file", req) + return await axiosInstance.post("feed/import", formData, { + headers: { + "Content-Type": "multipart/form-data", + }, + }) + }, + }, + user: { + login: async (req: LoginRequest) => await axiosInstance.post("user/login", req), + register: async (req: RegistrationRequest) => await axiosInstance.post("user/register", req), + passwordReset: async (req: PasswordResetRequest) => await axiosInstance.post("user/passwordReset", req), + getSettings: async () => await axiosInstance.get("user/settings"), + saveSettings: async (settings: Settings) => await axiosInstance.post("user/settings", settings), + getProfile: async () => await axiosInstance.get("user/profile"), + saveProfile: async (req: ProfileModificationRequest) => await axiosInstance.post("user/profile", req), + deleteProfile: async () => await axiosInstance.post("user/profile/deleteAccount"), + }, + server: { + getServerInfos: async () => await axiosInstance.get("server/get"), + }, + admin: { + getAllUsers: async () => await axiosInstance.get("admin/user/getAll"), + saveUser: async (req: AdminSaveUserRequest) => await axiosInstance.post("admin/user/save", req), + deleteUser: async (req: IDRequest) => await axiosInstance.post("admin/user/delete", req), + getMetrics: async () => await axiosInstance.get("admin/metrics"), + }, +} + +/** + * transform an error object to an array of strings that can be displayed to the user + * @param err an error object (e.g. from axios) + * @returns an array of messages to show the user + */ +export const errorToStrings = (err: unknown) => { + let strings: string[] = [] + + if (axios.isAxiosError(err) && err.response) { + if (typeof err.response.data === "string") strings.push(err.response.data) + if (isMessageError(err)) strings.push(err.response.data.message) + if (isMessageArrayError(err)) strings = [...strings, ...err.response.data.errors] + } + + return strings +} + +function isMessageError(err: AxiosError): err is AxiosError<{ message: string }> { + return !!err.response && !!err.response.data && typeof err.response.data === "object" && "message" in err.response.data +} + +function isMessageArrayError(err: AxiosError): err is AxiosError<{ errors: string[] }> { + return !!err.response && !!err.response.data && typeof err.response.data === "object" && "errors" in err.response.data +} diff --git a/commafeed-client/src/app/constants.ts b/commafeed-client/src/app/constants.ts index 2bf8903e..d1ce70f0 100644 --- a/commafeed-client/src/app/constants.ts +++ b/commafeed-client/src/app/constants.ts @@ -1,112 +1,112 @@ -import { t } from "@lingui/macro" -import { type IconType } from "react-icons" -import { FaAt } from "react-icons/fa" -import { SiBuffer, SiFacebook, SiGmail, SiInstapaper, SiPocket, SiTumblr, SiTwitter } from "react-icons/si" -import { type Category, type Entry, type SharingSettings } from "./types" - -const categories: Record = { - all: { - id: "all", - name: t`All`, - expanded: false, - children: [], - feeds: [], - position: 0, - }, - starred: { - id: "starred", - name: t`Starred`, - expanded: false, - children: [], - feeds: [], - position: 1, - }, -} - -const sharing: { - [key in keyof SharingSettings]: { - label: string - icon: IconType - color: `#${string}` - url: (url: string, description: string) => string - } -} = { - email: { - label: "Email", - icon: FaAt, - color: "#000000", - url: (url, desc) => `mailto:?subject=${desc}&body=${url}`, - }, - gmail: { - label: "Gmail", - icon: SiGmail, - color: "#EA4335", - url: (url, desc) => `https://mail.google.com/mail/?view=cm&fs=1&tf=1&source=mailto&su=${desc}&body=${url}`, - }, - facebook: { - label: "Facebook", - icon: SiFacebook, - color: "#1B74E4", - url: url => `https://www.facebook.com/sharer/sharer.php?u=${url}`, - }, - twitter: { - label: "Twitter", - icon: SiTwitter, - color: "#1D9BF0", - url: (url, desc) => `https://twitter.com/share?text=${desc}&url=${url}`, - }, - tumblr: { - label: "Tumblr", - icon: SiTumblr, - color: "#375672", - url: (url, desc) => `https://www.tumblr.com/share/link?url=${url}&name=${desc}`, - }, - pocket: { - label: "Pocket", - icon: SiPocket, - color: "#EF4154", - url: (url, desc) => `https://getpocket.com/save?url=${url}&title=${desc}`, - }, - instapaper: { - label: "Instapaper", - icon: SiInstapaper, - color: "#010101", - url: (url, desc) => `https://www.instapaper.com/hello2?url=${url}&title=${desc}`, - }, - buffer: { - label: "Buffer", - icon: SiBuffer, - color: "#000000", - url: (url, desc) => `https://bufferapp.com/add?url=${url}&text=${desc}`, - }, -} - -export const Constants = { - categories, - sharing, - layout: { - mobileBreakpoint: 992, - mobileBreakpointName: "md", - headerHeight: 60, - entryMaxWidth: 650, - isTopVisible: (div: HTMLElement) => { - const header = document.getElementById(Constants.dom.headerId)?.getBoundingClientRect() - return div.getBoundingClientRect().top >= (header?.bottom ?? 0) - }, - isBottomVisible: (div: HTMLElement) => { - const footer = document.getElementById(Constants.dom.footerId)?.getBoundingClientRect() - return div.getBoundingClientRect().bottom <= (footer?.top ?? window.innerHeight) - }, - }, - dom: { - headerId: "header", - footerId: "footer", - entryId: (entry: Entry) => `entry-id-${entry.id}`, - entryContextMenuId: (entry: Entry) => entry.id, - }, - tooltip: { - delay: 500, - }, - browserExtensionUrl: "https://github.com/Athou/commafeed-browser-extension", - bitcoinWalletAddress: "1dymfUxqCWpyD7a6rQSqNy4rLVDBsAr5e", -} +import { t } from "@lingui/macro" +import type { IconType } from "react-icons" +import { FaAt } from "react-icons/fa" +import { SiBuffer, SiFacebook, SiGmail, SiInstapaper, SiPocket, SiTumblr, SiTwitter } from "react-icons/si" +import type { Category, Entry, SharingSettings } from "./types" + +const categories: Record = { + all: { + id: "all", + name: t`All`, + expanded: false, + children: [], + feeds: [], + position: 0, + }, + starred: { + id: "starred", + name: t`Starred`, + expanded: false, + children: [], + feeds: [], + position: 1, + }, +} + +const sharing: { + [key in keyof SharingSettings]: { + label: string + icon: IconType + color: `#${string}` + url: (url: string, description: string) => string + } +} = { + email: { + label: "Email", + icon: FaAt, + color: "#000000", + url: (url, desc) => `mailto:?subject=${desc}&body=${url}`, + }, + gmail: { + label: "Gmail", + icon: SiGmail, + color: "#EA4335", + url: (url, desc) => `https://mail.google.com/mail/?view=cm&fs=1&tf=1&source=mailto&su=${desc}&body=${url}`, + }, + facebook: { + label: "Facebook", + icon: SiFacebook, + color: "#1B74E4", + url: url => `https://www.facebook.com/sharer/sharer.php?u=${url}`, + }, + twitter: { + label: "Twitter", + icon: SiTwitter, + color: "#1D9BF0", + url: (url, desc) => `https://twitter.com/share?text=${desc}&url=${url}`, + }, + tumblr: { + label: "Tumblr", + icon: SiTumblr, + color: "#375672", + url: (url, desc) => `https://www.tumblr.com/share/link?url=${url}&name=${desc}`, + }, + pocket: { + label: "Pocket", + icon: SiPocket, + color: "#EF4154", + url: (url, desc) => `https://getpocket.com/save?url=${url}&title=${desc}`, + }, + instapaper: { + label: "Instapaper", + icon: SiInstapaper, + color: "#010101", + url: (url, desc) => `https://www.instapaper.com/hello2?url=${url}&title=${desc}`, + }, + buffer: { + label: "Buffer", + icon: SiBuffer, + color: "#000000", + url: (url, desc) => `https://bufferapp.com/add?url=${url}&text=${desc}`, + }, +} + +export const Constants = { + categories, + sharing, + layout: { + mobileBreakpoint: 992, + mobileBreakpointName: "md", + headerHeight: 60, + entryMaxWidth: 650, + isTopVisible: (div: HTMLElement) => { + const header = document.getElementById(Constants.dom.headerId)?.getBoundingClientRect() + return div.getBoundingClientRect().top >= (header?.bottom ?? 0) + }, + isBottomVisible: (div: HTMLElement) => { + const footer = document.getElementById(Constants.dom.footerId)?.getBoundingClientRect() + return div.getBoundingClientRect().bottom <= (footer?.top ?? window.innerHeight) + }, + }, + dom: { + headerId: "header", + footerId: "footer", + entryId: (entry: Entry) => `entry-id-${entry.id}`, + entryContextMenuId: (entry: Entry) => entry.id, + }, + tooltip: { + delay: 500, + }, + browserExtensionUrl: "https://github.com/Athou/commafeed-browser-extension", + bitcoinWalletAddress: "1dymfUxqCWpyD7a6rQSqNy4rLVDBsAr5e", +} diff --git a/commafeed-client/src/app/entries/entries.test.ts b/commafeed-client/src/app/entries/entries.test.ts index 46616937..3ccf4515 100644 --- a/commafeed-client/src/app/entries/entries.test.ts +++ b/commafeed-client/src/app/entries/entries.test.ts @@ -1,145 +1,145 @@ -import { configureStore } from "@reduxjs/toolkit" -import { type client } from "app/client" -import { loadEntries, loadMoreEntries, markAllEntries, markEntry } from "app/entries/thunks" -import { reducers, type RootState } from "app/store" -import { type Entries, type Entry } from "app/types" -import { type AxiosResponse } from "axios" -import { beforeEach, describe, expect, it, vi } from "vitest" -import { mockReset } from "vitest-mock-extended" - -const mockClient = await vi.hoisted(async () => { - const mockModule = await import("vitest-mock-extended") - return mockModule.mockDeep() -}) -vi.mock("app/client", () => ({ client: mockClient })) - -describe("entries", () => { - beforeEach(() => { - mockReset(mockClient) - }) - - it("loads entries", async () => { - mockClient.feed.getEntries.mockResolvedValue({ - data: { - entries: [{ id: "3" } as Entry], - hasMore: false, - name: "my-feed", - errorCount: 3, - feedLink: "https://mysite.com/feed", - timestamp: 123, - ignoredReadStatus: false, - }, - } as AxiosResponse) - - const store = configureStore({ reducer: reducers }) - const promise = store.dispatch(loadEntries({ source: { type: "feed", id: "feed-id" }, clearSearch: true })) - - expect(store.getState().entries.source.type).toBe("feed") - expect(store.getState().entries.source.id).toBe("feed-id") - expect(store.getState().entries.entries).toStrictEqual([]) - expect(store.getState().entries.hasMore).toBe(true) - expect(store.getState().entries.sourceLabel).toBe("") - expect(store.getState().entries.sourceWebsiteUrl).toBe("") - expect(store.getState().entries.timestamp).toBeUndefined() - - await promise - expect(store.getState().entries.source.type).toBe("feed") - expect(store.getState().entries.source.id).toBe("feed-id") - expect(store.getState().entries.entries).toStrictEqual([{ id: "3" }]) - expect(store.getState().entries.hasMore).toBe(false) - expect(store.getState().entries.sourceLabel).toBe("my-feed") - expect(store.getState().entries.sourceWebsiteUrl).toBe("https://mysite.com/feed") - expect(store.getState().entries.timestamp).toBe(123) - }) - - it("loads more entries", async () => { - mockClient.category.getEntries.mockResolvedValue({ - data: { - entries: [{ id: "4" } as Entry], - hasMore: false, - name: "my-feed", - errorCount: 3, - feedLink: "https://mysite.com/feed", - timestamp: 123, - ignoredReadStatus: false, - }, - } as AxiosResponse) - - const store = configureStore({ - reducer: reducers, - preloadedState: { - entries: { - source: { - type: "category", - id: "category-id", - }, - sourceLabel: "", - sourceWebsiteUrl: "", - entries: [{ id: "3" } as Entry], - hasMore: true, - loading: false, - scrollingToEntry: false, - }, - } as RootState, - }) - const promise = store.dispatch(loadMoreEntries()) - - await promise - expect(store.getState().entries.entries).toStrictEqual([{ id: "3" }, { id: "4" }]) - expect(store.getState().entries.hasMore).toBe(false) - }) - - it("marks an entry as read", () => { - const store = configureStore({ - reducer: reducers, - preloadedState: { - entries: { - source: { - type: "category", - id: "category-id", - }, - sourceLabel: "", - sourceWebsiteUrl: "", - entries: [{ id: "3", read: false } as Entry, { id: "4", read: false } as Entry], - hasMore: true, - loading: false, - scrollingToEntry: false, - }, - } as RootState, - }) - - store.dispatch(markEntry({ entry: { id: "3" } as Entry, read: true })) - expect(store.getState().entries.entries).toStrictEqual([ - { id: "3", read: true }, - { id: "4", read: false }, - ]) - expect(mockClient.entry.mark).toHaveBeenCalledWith({ id: "3", read: true }) - }) - - it("marks all entries as read", () => { - const store = configureStore({ - reducer: reducers, - preloadedState: { - entries: { - source: { - type: "category", - id: "category-id", - }, - sourceLabel: "", - sourceWebsiteUrl: "", - entries: [{ id: "3", read: false } as Entry, { id: "4", read: false } as Entry], - hasMore: true, - loading: false, - scrollingToEntry: false, - }, - } as RootState, - }) - - store.dispatch(markAllEntries({ sourceType: "category", req: { id: "all", read: true } })) - expect(store.getState().entries.entries).toStrictEqual([ - { id: "3", read: true }, - { id: "4", read: true }, - ]) - expect(mockClient.category.markEntries).toHaveBeenCalledWith({ id: "all", read: true }) - }) -}) +import { configureStore } from "@reduxjs/toolkit" +import type { client } from "app/client" +import { loadEntries, loadMoreEntries, markAllEntries, markEntry } from "app/entries/thunks" +import { type RootState, reducers } from "app/store" +import type { Entries, Entry } from "app/types" +import type { AxiosResponse } from "axios" +import { beforeEach, describe, expect, it, vi } from "vitest" +import { mockReset } from "vitest-mock-extended" + +const mockClient = await vi.hoisted(async () => { + const mockModule = await import("vitest-mock-extended") + return mockModule.mockDeep() +}) +vi.mock("app/client", () => ({ client: mockClient })) + +describe("entries", () => { + beforeEach(() => { + mockReset(mockClient) + }) + + it("loads entries", async () => { + mockClient.feed.getEntries.mockResolvedValue({ + data: { + entries: [{ id: "3" } as Entry], + hasMore: false, + name: "my-feed", + errorCount: 3, + feedLink: "https://mysite.com/feed", + timestamp: 123, + ignoredReadStatus: false, + }, + } as AxiosResponse) + + const store = configureStore({ reducer: reducers }) + const promise = store.dispatch(loadEntries({ source: { type: "feed", id: "feed-id" }, clearSearch: true })) + + expect(store.getState().entries.source.type).toBe("feed") + expect(store.getState().entries.source.id).toBe("feed-id") + expect(store.getState().entries.entries).toStrictEqual([]) + expect(store.getState().entries.hasMore).toBe(true) + expect(store.getState().entries.sourceLabel).toBe("") + expect(store.getState().entries.sourceWebsiteUrl).toBe("") + expect(store.getState().entries.timestamp).toBeUndefined() + + await promise + expect(store.getState().entries.source.type).toBe("feed") + expect(store.getState().entries.source.id).toBe("feed-id") + expect(store.getState().entries.entries).toStrictEqual([{ id: "3" }]) + expect(store.getState().entries.hasMore).toBe(false) + expect(store.getState().entries.sourceLabel).toBe("my-feed") + expect(store.getState().entries.sourceWebsiteUrl).toBe("https://mysite.com/feed") + expect(store.getState().entries.timestamp).toBe(123) + }) + + it("loads more entries", async () => { + mockClient.category.getEntries.mockResolvedValue({ + data: { + entries: [{ id: "4" } as Entry], + hasMore: false, + name: "my-feed", + errorCount: 3, + feedLink: "https://mysite.com/feed", + timestamp: 123, + ignoredReadStatus: false, + }, + } as AxiosResponse) + + const store = configureStore({ + reducer: reducers, + preloadedState: { + entries: { + source: { + type: "category", + id: "category-id", + }, + sourceLabel: "", + sourceWebsiteUrl: "", + entries: [{ id: "3" } as Entry], + hasMore: true, + loading: false, + scrollingToEntry: false, + }, + } as RootState, + }) + const promise = store.dispatch(loadMoreEntries()) + + await promise + expect(store.getState().entries.entries).toStrictEqual([{ id: "3" }, { id: "4" }]) + expect(store.getState().entries.hasMore).toBe(false) + }) + + it("marks an entry as read", () => { + const store = configureStore({ + reducer: reducers, + preloadedState: { + entries: { + source: { + type: "category", + id: "category-id", + }, + sourceLabel: "", + sourceWebsiteUrl: "", + entries: [{ id: "3", read: false } as Entry, { id: "4", read: false } as Entry], + hasMore: true, + loading: false, + scrollingToEntry: false, + }, + } as RootState, + }) + + store.dispatch(markEntry({ entry: { id: "3" } as Entry, read: true })) + expect(store.getState().entries.entries).toStrictEqual([ + { id: "3", read: true }, + { id: "4", read: false }, + ]) + expect(mockClient.entry.mark).toHaveBeenCalledWith({ id: "3", read: true }) + }) + + it("marks all entries as read", () => { + const store = configureStore({ + reducer: reducers, + preloadedState: { + entries: { + source: { + type: "category", + id: "category-id", + }, + sourceLabel: "", + sourceWebsiteUrl: "", + entries: [{ id: "3", read: false } as Entry, { id: "4", read: false } as Entry], + hasMore: true, + loading: false, + scrollingToEntry: false, + }, + } as RootState, + }) + + store.dispatch(markAllEntries({ sourceType: "category", req: { id: "all", read: true } })) + expect(store.getState().entries.entries).toStrictEqual([ + { id: "3", read: true }, + { id: "4", read: true }, + ]) + expect(mockClient.category.markEntries).toHaveBeenCalledWith({ id: "all", read: true }) + }) +}) diff --git a/commafeed-client/src/app/entries/slice.ts b/commafeed-client/src/app/entries/slice.ts index 2381c632..8762e489 100644 --- a/commafeed-client/src/app/entries/slice.ts +++ b/commafeed-client/src/app/entries/slice.ts @@ -1,134 +1,122 @@ -import { createSlice, type PayloadAction } from "@reduxjs/toolkit" -import { Constants } from "app/constants" -import { loadEntries, loadMoreEntries, markAllEntries, markEntry, markMultipleEntries, starEntry, tagEntry } from "app/entries/thunks" -import { type Entry } from "app/types" - -export type EntrySourceType = "category" | "feed" | "tag" - -export interface EntrySource { - type: EntrySourceType - id: string -} - -export type ExpendableEntry = Entry & { expanded?: boolean } - -interface EntriesState { - /** selected source */ - source: EntrySource - sourceLabel: string - sourceWebsiteUrl: string - entries: ExpendableEntry[] - /** stores when the first batch of entries were retrieved - * - * this is used when marking all entries of a feed/category to only mark entries up to that timestamp as newer entries were potentially never shown - */ - timestamp?: number - selectedEntryId?: string - hasMore: boolean - loading: boolean - search?: string - scrollingToEntry: boolean -} - -const initialState: EntriesState = { - source: { - type: "category", - id: Constants.categories.all.id, - }, - sourceLabel: "", - sourceWebsiteUrl: "", - entries: [], - hasMore: true, - loading: false, - scrollingToEntry: false, -} - -export const entriesSlice = createSlice({ - name: "entries", - initialState, - reducers: { - setSelectedEntry: (state, action: PayloadAction) => { - state.selectedEntryId = action.payload.id - }, - setEntryExpanded: (state, action: PayloadAction<{ entry: Entry; expanded: boolean }>) => { - state.entries - .filter(e => e.id === action.payload.entry.id) - .forEach(e => { - e.expanded = action.payload.expanded - }) - }, - setScrollingToEntry: (state, action: PayloadAction) => { - state.scrollingToEntry = action.payload - }, - setSearch: (state, action: PayloadAction) => { - state.search = action.payload - }, - }, - extraReducers: builder => { - builder.addCase(markEntry.pending, (state, action) => { - state.entries - .filter(e => e.id === action.meta.arg.entry.id) - .forEach(e => { - e.read = action.meta.arg.read - }) - }) - builder.addCase(markMultipleEntries.pending, (state, action) => { - state.entries - .filter(e => action.meta.arg.entries.some(e2 => e2.id === e.id)) - .forEach(e => { - e.read = action.meta.arg.read - }) - }) - builder.addCase(markAllEntries.pending, (state, action) => { - state.entries - .filter(e => (action.meta.arg.req.olderThan ? e.date < action.meta.arg.req.olderThan : true)) - .forEach(e => { - e.read = true - }) - }) - builder.addCase(starEntry.pending, (state, action) => { - state.entries - .filter(e => action.meta.arg.entry.id === e.id && action.meta.arg.entry.feedId === e.feedId) - .forEach(e => { - e.starred = action.meta.arg.starred - }) - }) - builder.addCase(loadEntries.pending, (state, action) => { - state.source = action.meta.arg.source - state.entries = [] - state.timestamp = undefined - state.sourceLabel = "" - state.sourceWebsiteUrl = "" - state.hasMore = true - state.selectedEntryId = undefined - state.loading = true - }) - builder.addCase(loadMoreEntries.pending, state => { - state.loading = true - }) - builder.addCase(loadEntries.fulfilled, (state, action) => { - state.entries = action.payload.entries - state.timestamp = action.payload.timestamp - state.sourceLabel = action.payload.name - state.sourceWebsiteUrl = action.payload.feedLink - state.hasMore = action.payload.hasMore - state.loading = false - }) - builder.addCase(loadMoreEntries.fulfilled, (state, action) => { - // remove already existing entries - const entriesToAdd = action.payload.entries.filter(e => !state.entries.some(e2 => e.id === e2.id)) - state.entries = [...state.entries, ...entriesToAdd] - state.hasMore = action.payload.hasMore - state.loading = false - }) - builder.addCase(tagEntry.pending, (state, action) => { - state.entries - .filter(e => +e.id === action.meta.arg.entryId) - .forEach(e => { - e.tags = action.meta.arg.tags - }) - }) - }, -}) - -export const { setSearch } = entriesSlice.actions +import { type PayloadAction, createSlice } from "@reduxjs/toolkit" +import { Constants } from "app/constants" +import { loadEntries, loadMoreEntries, markAllEntries, markEntry, markMultipleEntries, starEntry, tagEntry } from "app/entries/thunks" +import type { Entry } from "app/types" + +export type EntrySourceType = "category" | "feed" | "tag" + +export interface EntrySource { + type: EntrySourceType + id: string +} + +export type ExpendableEntry = Entry & { expanded?: boolean } + +interface EntriesState { + /** selected source */ + source: EntrySource + sourceLabel: string + sourceWebsiteUrl: string + entries: ExpendableEntry[] + /** stores when the first batch of entries were retrieved + * + * this is used when marking all entries of a feed/category to only mark entries up to that timestamp as newer entries were potentially never shown + */ + timestamp?: number + selectedEntryId?: string + hasMore: boolean + loading: boolean + search?: string + scrollingToEntry: boolean +} + +const initialState: EntriesState = { + source: { + type: "category", + id: Constants.categories.all.id, + }, + sourceLabel: "", + sourceWebsiteUrl: "", + entries: [], + hasMore: true, + loading: false, + scrollingToEntry: false, +} + +export const entriesSlice = createSlice({ + name: "entries", + initialState, + reducers: { + setSelectedEntry: (state, action: PayloadAction) => { + state.selectedEntryId = action.payload.id + }, + setEntryExpanded: (state, action: PayloadAction<{ entry: Entry; expanded: boolean }>) => { + for (const e of state.entries.filter(e => e.id === action.payload.entry.id)) { + e.expanded = action.payload.expanded + } + }, + setScrollingToEntry: (state, action: PayloadAction) => { + state.scrollingToEntry = action.payload + }, + setSearch: (state, action: PayloadAction) => { + state.search = action.payload + }, + }, + extraReducers: builder => { + builder.addCase(markEntry.pending, (state, action) => { + for (const e of state.entries.filter(e => e.id === action.meta.arg.entry.id)) { + e.read = action.meta.arg.read + } + }) + builder.addCase(markMultipleEntries.pending, (state, action) => { + for (const e of state.entries.filter(e => action.meta.arg.entries.some(e2 => e2.id === e.id))) { + e.read = action.meta.arg.read + } + }) + builder.addCase(markAllEntries.pending, (state, action) => { + for (const e of state.entries.filter(e => (action.meta.arg.req.olderThan ? e.date < action.meta.arg.req.olderThan : true))) { + e.read = true + } + }) + builder.addCase(starEntry.pending, (state, action) => { + for (const e of state.entries.filter(e => action.meta.arg.entry.id === e.id && action.meta.arg.entry.feedId === e.feedId)) { + e.starred = action.meta.arg.starred + } + }) + builder.addCase(loadEntries.pending, (state, action) => { + state.source = action.meta.arg.source + state.entries = [] + state.timestamp = undefined + state.sourceLabel = "" + state.sourceWebsiteUrl = "" + state.hasMore = true + state.selectedEntryId = undefined + state.loading = true + }) + builder.addCase(loadMoreEntries.pending, state => { + state.loading = true + }) + builder.addCase(loadEntries.fulfilled, (state, action) => { + state.entries = action.payload.entries + state.timestamp = action.payload.timestamp + state.sourceLabel = action.payload.name + state.sourceWebsiteUrl = action.payload.feedLink + state.hasMore = action.payload.hasMore + state.loading = false + }) + builder.addCase(loadMoreEntries.fulfilled, (state, action) => { + // remove already existing entries + const entriesToAdd = action.payload.entries.filter(e => !state.entries.some(e2 => e.id === e2.id)) + state.entries = [...state.entries, ...entriesToAdd] + state.hasMore = action.payload.hasMore + state.loading = false + }) + builder.addCase(tagEntry.pending, (state, action) => { + for (const e of state.entries.filter(e => +e.id === action.meta.arg.entryId)) { + e.tags = action.meta.arg.tags + } + }) + }, +}) + +export const { setSearch } = entriesSlice.actions diff --git a/commafeed-client/src/app/entries/thunks.ts b/commafeed-client/src/app/entries/thunks.ts index 5484b36b..7523636d 100644 --- a/commafeed-client/src/app/entries/thunks.ts +++ b/commafeed-client/src/app/entries/thunks.ts @@ -1,247 +1,247 @@ -import { createAppAsyncThunk } from "app/async-thunk" -import { client } from "app/client" -import { Constants } from "app/constants" -import { entriesSlice, type EntrySource, type EntrySourceType, setSearch } from "app/entries/slice" -import type { RootState } from "app/store" -import { reloadTree } from "app/tree/thunks" -import type { Entry, MarkRequest, TagRequest } from "app/types" -import { reloadTags } from "app/user/thunks" -import { scrollToWithCallback } from "app/utils" -import { flushSync } from "react-dom" - -const getEndpoint = (sourceType: EntrySourceType) => - sourceType === "category" || sourceType === "tag" ? client.category.getEntries : client.feed.getEntries -export const loadEntries = createAppAsyncThunk( - "entries/load", - async ( - arg: { - source: EntrySource - clearSearch: boolean - }, - thunkApi - ) => { - if (arg.clearSearch) thunkApi.dispatch(setSearch("")) - - const state = thunkApi.getState() - const endpoint = getEndpoint(arg.source.type) - const result = await endpoint(buildGetEntriesPaginatedRequest(state, arg.source, 0)) - return result.data - } -) -export const loadMoreEntries = createAppAsyncThunk("entries/loadMore", async (_, thunkApi) => { - const state = thunkApi.getState() - const { source } = state.entries - const offset = - state.user.settings?.readingMode === "all" ? state.entries.entries.length : state.entries.entries.filter(e => !e.read).length - const endpoint = getEndpoint(state.entries.source.type) - const result = await endpoint(buildGetEntriesPaginatedRequest(state, source, offset)) - return result.data -}) -const buildGetEntriesPaginatedRequest = (state: RootState, source: EntrySource, offset: number) => ({ - id: source.type === "tag" ? Constants.categories.all.id : source.id, - order: state.user.settings?.readingOrder, - readType: state.user.settings?.readingMode, - offset, - limit: 50, - tag: source.type === "tag" ? source.id : undefined, - keywords: state.entries.search, -}) -export const reloadEntries = createAppAsyncThunk("entries/reload", (arg, thunkApi) => { - const state = thunkApi.getState() - thunkApi.dispatch(loadEntries({ source: state.entries.source, clearSearch: false })) -}) -export const search = createAppAsyncThunk("entries/search", (arg: string, thunkApi) => { - const state = thunkApi.getState() - thunkApi.dispatch(setSearch(arg)) - thunkApi.dispatch(loadEntries({ source: state.entries.source, clearSearch: false })) -}) -export const markEntry = createAppAsyncThunk( - "entries/entry/mark", - (arg: { entry: Entry; read: boolean }) => { - client.entry.mark({ - id: arg.entry.id, - read: arg.read, - }) - }, - { - condition: arg => arg.entry.markable && arg.entry.read !== arg.read, - } -) -export const markMultipleEntries = createAppAsyncThunk( - "entries/entry/markMultiple", - async ( - arg: { - entries: Entry[] - read: boolean - }, - thunkApi - ) => { - const requests: MarkRequest[] = arg.entries.map(e => ({ - id: e.id, - read: arg.read, - })) - await client.entry.markMultiple({ requests }) - thunkApi.dispatch(reloadTree()) - } -) -export const markEntriesUpToEntry = createAppAsyncThunk("entries/entry/upToEntry", (arg: Entry, thunkApi) => { - const state = thunkApi.getState() - const { entries } = state.entries - - const index = entries.findIndex(e => e.id === arg.id) - if (index === -1) return - - thunkApi.dispatch( - markMultipleEntries({ - entries: entries.slice(0, index + 1), - read: true, - }) - ) -}) -export const markAllEntries = createAppAsyncThunk( - "entries/entry/markAll", - async ( - arg: { - sourceType: EntrySourceType - req: MarkRequest - }, - thunkApi - ) => { - const endpoint = arg.sourceType === "category" ? client.category.markEntries : client.feed.markEntries - await endpoint(arg.req) - thunkApi.dispatch(reloadEntries()) - thunkApi.dispatch(reloadTree()) - } -) -export const starEntry = createAppAsyncThunk( - "entries/entry/star", - (arg: { entry: Entry; starred: boolean }) => { - client.entry.star({ - id: arg.entry.id, - feedId: +arg.entry.feedId, - starred: arg.starred, - }) - }, - { - condition: arg => arg.entry.markable && arg.entry.starred !== arg.starred, - } -) -export const selectEntry = createAppAsyncThunk( - "entries/entry/select", - ( - arg: { - entry: Entry - expand: boolean - markAsRead: boolean - scrollToEntry: boolean - }, - thunkApi - ) => { - const state = thunkApi.getState() - const entry = state.entries.entries.find(e => e.id === arg.entry.id) - if (!entry) return - - // flushSync is required because we need the newly selected entry to be expanded - // and the previously selected entry to be collapsed to be able to scroll to the right position - flushSync(() => { - // mark as read if requested - if (arg.markAsRead) { - thunkApi.dispatch(markEntry({ entry, read: true })) - } - - // set entry as selected - thunkApi.dispatch(entriesSlice.actions.setSelectedEntry(entry)) - - // expand if requested - const previouslySelectedEntry = state.entries.entries.find(e => e.id === state.entries.selectedEntryId) - if (previouslySelectedEntry) { - thunkApi.dispatch( - entriesSlice.actions.setEntryExpanded({ - entry: previouslySelectedEntry, - expanded: false, - }) - ) - } - thunkApi.dispatch(entriesSlice.actions.setEntryExpanded({ entry, expanded: arg.expand })) - }) - - if (arg.scrollToEntry) { - const entryElement = document.getElementById(Constants.dom.entryId(entry)) - if (entryElement) { - const scrollMode = state.user.settings?.scrollMode - const entryEntirelyVisible = Constants.layout.isTopVisible(entryElement) && Constants.layout.isBottomVisible(entryElement) - if (scrollMode === "always" || (scrollMode === "if_needed" && !entryEntirelyVisible)) { - const scrollSpeed = state.user.settings?.scrollSpeed - thunkApi.dispatch(entriesSlice.actions.setScrollingToEntry(true)) - scrollToEntry(entryElement, scrollSpeed, () => thunkApi.dispatch(entriesSlice.actions.setScrollingToEntry(false))) - } - } - } - } -) -const scrollToEntry = (entryElement: HTMLElement, scrollSpeed: number | undefined, onScrollEnded: () => void) => { - const header = document.getElementById(Constants.dom.headerId)?.getBoundingClientRect() - const offset = (header?.bottom ?? 0) + 3 - scrollToWithCallback({ - options: { - top: entryElement.offsetTop - offset, - behavior: scrollSpeed && scrollSpeed > 0 ? "smooth" : "auto", - }, - onScrollEnded, - }) -} - -export const selectPreviousEntry = createAppAsyncThunk( - "entries/entry/selectPrevious", - ( - arg: { - expand: boolean - markAsRead: boolean - scrollToEntry: boolean - }, - thunkApi - ) => { - const state = thunkApi.getState() - const { entries } = state.entries - const previousIndex = entries.findIndex(e => e.id === state.entries.selectedEntryId) - 1 - if (previousIndex >= 0) { - thunkApi.dispatch( - selectEntry({ - entry: entries[previousIndex], - expand: arg.expand, - markAsRead: arg.markAsRead, - scrollToEntry: arg.scrollToEntry, - }) - ) - } - } -) -export const selectNextEntry = createAppAsyncThunk( - "entries/entry/selectNext", - ( - arg: { - expand: boolean - markAsRead: boolean - scrollToEntry: boolean - }, - thunkApi - ) => { - const state = thunkApi.getState() - const { entries } = state.entries - const nextIndex = entries.findIndex(e => e.id === state.entries.selectedEntryId) + 1 - if (nextIndex < entries.length) { - thunkApi.dispatch( - selectEntry({ - entry: entries[nextIndex], - expand: arg.expand, - markAsRead: arg.markAsRead, - scrollToEntry: arg.scrollToEntry, - }) - ) - } - } -) -export const tagEntry = createAppAsyncThunk("entries/entry/tag", async (arg: TagRequest, thunkApi) => { - await client.entry.tag(arg) - thunkApi.dispatch(reloadTags()) -}) +import { createAppAsyncThunk } from "app/async-thunk" +import { client } from "app/client" +import { Constants } from "app/constants" +import { type EntrySource, type EntrySourceType, entriesSlice, setSearch } from "app/entries/slice" +import type { RootState } from "app/store" +import { reloadTree } from "app/tree/thunks" +import type { Entry, MarkRequest, TagRequest } from "app/types" +import { reloadTags } from "app/user/thunks" +import { scrollToWithCallback } from "app/utils" +import { flushSync } from "react-dom" + +const getEndpoint = (sourceType: EntrySourceType) => + sourceType === "category" || sourceType === "tag" ? client.category.getEntries : client.feed.getEntries +export const loadEntries = createAppAsyncThunk( + "entries/load", + async ( + arg: { + source: EntrySource + clearSearch: boolean + }, + thunkApi + ) => { + if (arg.clearSearch) thunkApi.dispatch(setSearch("")) + + const state = thunkApi.getState() + const endpoint = getEndpoint(arg.source.type) + const result = await endpoint(buildGetEntriesPaginatedRequest(state, arg.source, 0)) + return result.data + } +) +export const loadMoreEntries = createAppAsyncThunk("entries/loadMore", async (_, thunkApi) => { + const state = thunkApi.getState() + const { source } = state.entries + const offset = + state.user.settings?.readingMode === "all" ? state.entries.entries.length : state.entries.entries.filter(e => !e.read).length + const endpoint = getEndpoint(state.entries.source.type) + const result = await endpoint(buildGetEntriesPaginatedRequest(state, source, offset)) + return result.data +}) +const buildGetEntriesPaginatedRequest = (state: RootState, source: EntrySource, offset: number) => ({ + id: source.type === "tag" ? Constants.categories.all.id : source.id, + order: state.user.settings?.readingOrder, + readType: state.user.settings?.readingMode, + offset, + limit: 50, + tag: source.type === "tag" ? source.id : undefined, + keywords: state.entries.search, +}) +export const reloadEntries = createAppAsyncThunk("entries/reload", (arg, thunkApi) => { + const state = thunkApi.getState() + thunkApi.dispatch(loadEntries({ source: state.entries.source, clearSearch: false })) +}) +export const search = createAppAsyncThunk("entries/search", (arg: string, thunkApi) => { + const state = thunkApi.getState() + thunkApi.dispatch(setSearch(arg)) + thunkApi.dispatch(loadEntries({ source: state.entries.source, clearSearch: false })) +}) +export const markEntry = createAppAsyncThunk( + "entries/entry/mark", + (arg: { entry: Entry; read: boolean }) => { + client.entry.mark({ + id: arg.entry.id, + read: arg.read, + }) + }, + { + condition: arg => arg.entry.markable && arg.entry.read !== arg.read, + } +) +export const markMultipleEntries = createAppAsyncThunk( + "entries/entry/markMultiple", + async ( + arg: { + entries: Entry[] + read: boolean + }, + thunkApi + ) => { + const requests: MarkRequest[] = arg.entries.map(e => ({ + id: e.id, + read: arg.read, + })) + await client.entry.markMultiple({ requests }) + thunkApi.dispatch(reloadTree()) + } +) +export const markEntriesUpToEntry = createAppAsyncThunk("entries/entry/upToEntry", (arg: Entry, thunkApi) => { + const state = thunkApi.getState() + const { entries } = state.entries + + const index = entries.findIndex(e => e.id === arg.id) + if (index === -1) return + + thunkApi.dispatch( + markMultipleEntries({ + entries: entries.slice(0, index + 1), + read: true, + }) + ) +}) +export const markAllEntries = createAppAsyncThunk( + "entries/entry/markAll", + async ( + arg: { + sourceType: EntrySourceType + req: MarkRequest + }, + thunkApi + ) => { + const endpoint = arg.sourceType === "category" ? client.category.markEntries : client.feed.markEntries + await endpoint(arg.req) + thunkApi.dispatch(reloadEntries()) + thunkApi.dispatch(reloadTree()) + } +) +export const starEntry = createAppAsyncThunk( + "entries/entry/star", + (arg: { entry: Entry; starred: boolean }) => { + client.entry.star({ + id: arg.entry.id, + feedId: +arg.entry.feedId, + starred: arg.starred, + }) + }, + { + condition: arg => arg.entry.markable && arg.entry.starred !== arg.starred, + } +) +export const selectEntry = createAppAsyncThunk( + "entries/entry/select", + ( + arg: { + entry: Entry + expand: boolean + markAsRead: boolean + scrollToEntry: boolean + }, + thunkApi + ) => { + const state = thunkApi.getState() + const entry = state.entries.entries.find(e => e.id === arg.entry.id) + if (!entry) return + + // flushSync is required because we need the newly selected entry to be expanded + // and the previously selected entry to be collapsed to be able to scroll to the right position + flushSync(() => { + // mark as read if requested + if (arg.markAsRead) { + thunkApi.dispatch(markEntry({ entry, read: true })) + } + + // set entry as selected + thunkApi.dispatch(entriesSlice.actions.setSelectedEntry(entry)) + + // expand if requested + const previouslySelectedEntry = state.entries.entries.find(e => e.id === state.entries.selectedEntryId) + if (previouslySelectedEntry) { + thunkApi.dispatch( + entriesSlice.actions.setEntryExpanded({ + entry: previouslySelectedEntry, + expanded: false, + }) + ) + } + thunkApi.dispatch(entriesSlice.actions.setEntryExpanded({ entry, expanded: arg.expand })) + }) + + if (arg.scrollToEntry) { + const entryElement = document.getElementById(Constants.dom.entryId(entry)) + if (entryElement) { + const scrollMode = state.user.settings?.scrollMode + const entryEntirelyVisible = Constants.layout.isTopVisible(entryElement) && Constants.layout.isBottomVisible(entryElement) + if (scrollMode === "always" || (scrollMode === "if_needed" && !entryEntirelyVisible)) { + const scrollSpeed = state.user.settings?.scrollSpeed + thunkApi.dispatch(entriesSlice.actions.setScrollingToEntry(true)) + scrollToEntry(entryElement, scrollSpeed, () => thunkApi.dispatch(entriesSlice.actions.setScrollingToEntry(false))) + } + } + } + } +) +const scrollToEntry = (entryElement: HTMLElement, scrollSpeed: number | undefined, onScrollEnded: () => void) => { + const header = document.getElementById(Constants.dom.headerId)?.getBoundingClientRect() + const offset = (header?.bottom ?? 0) + 3 + scrollToWithCallback({ + options: { + top: entryElement.offsetTop - offset, + behavior: scrollSpeed && scrollSpeed > 0 ? "smooth" : "auto", + }, + onScrollEnded, + }) +} + +export const selectPreviousEntry = createAppAsyncThunk( + "entries/entry/selectPrevious", + ( + arg: { + expand: boolean + markAsRead: boolean + scrollToEntry: boolean + }, + thunkApi + ) => { + const state = thunkApi.getState() + const { entries } = state.entries + const previousIndex = entries.findIndex(e => e.id === state.entries.selectedEntryId) - 1 + if (previousIndex >= 0) { + thunkApi.dispatch( + selectEntry({ + entry: entries[previousIndex], + expand: arg.expand, + markAsRead: arg.markAsRead, + scrollToEntry: arg.scrollToEntry, + }) + ) + } + } +) +export const selectNextEntry = createAppAsyncThunk( + "entries/entry/selectNext", + ( + arg: { + expand: boolean + markAsRead: boolean + scrollToEntry: boolean + }, + thunkApi + ) => { + const state = thunkApi.getState() + const { entries } = state.entries + const nextIndex = entries.findIndex(e => e.id === state.entries.selectedEntryId) + 1 + if (nextIndex < entries.length) { + thunkApi.dispatch( + selectEntry({ + entry: entries[nextIndex], + expand: arg.expand, + markAsRead: arg.markAsRead, + scrollToEntry: arg.scrollToEntry, + }) + ) + } + } +) +export const tagEntry = createAppAsyncThunk("entries/entry/tag", async (arg: TagRequest, thunkApi) => { + await client.entry.tag(arg) + thunkApi.dispatch(reloadTags()) +}) diff --git a/commafeed-client/src/app/redirect/redirect.test.ts b/commafeed-client/src/app/redirect/redirect.test.ts index 430ea80e..be6f86fd 100644 --- a/commafeed-client/src/app/redirect/redirect.test.ts +++ b/commafeed-client/src/app/redirect/redirect.test.ts @@ -1,10 +1,10 @@ -import { redirectToCategory } from "app/redirect/thunks" -import { store } from "app/store" -import { describe, expect, it } from "vitest" - -describe("redirects", () => { - it("redirects to category", async () => { - await store.dispatch(redirectToCategory("1")) - expect(store.getState().redirect.to).toBe("/app/category/1") - }) -}) +import { redirectToCategory } from "app/redirect/thunks" +import { store } from "app/store" +import { describe, expect, it } from "vitest" + +describe("redirects", () => { + it("redirects to category", async () => { + await store.dispatch(redirectToCategory("1")) + expect(store.getState().redirect.to).toBe("/app/category/1") + }) +}) diff --git a/commafeed-client/src/app/redirect/slice.ts b/commafeed-client/src/app/redirect/slice.ts index ae2cf6cb..3b8fb60c 100644 --- a/commafeed-client/src/app/redirect/slice.ts +++ b/commafeed-client/src/app/redirect/slice.ts @@ -1,19 +1,19 @@ -import { createSlice, type PayloadAction } from "@reduxjs/toolkit" - -interface RedirectState { - to?: string -} - -const initialState: RedirectState = {} - -export const redirectSlice = createSlice({ - name: "redirect", - initialState, - reducers: { - redirectTo: (state, action: PayloadAction) => { - state.to = action.payload - }, - }, -}) - -export const { redirectTo } = redirectSlice.actions +import { type PayloadAction, createSlice } from "@reduxjs/toolkit" + +interface RedirectState { + to?: string +} + +const initialState: RedirectState = {} + +export const redirectSlice = createSlice({ + name: "redirect", + initialState, + reducers: { + redirectTo: (state, action: PayloadAction) => { + state.to = action.payload + }, + }, +}) + +export const { redirectTo } = redirectSlice.actions diff --git a/commafeed-client/src/app/redirect/thunks.ts b/commafeed-client/src/app/redirect/thunks.ts index 4e44f688..981ce456 100644 --- a/commafeed-client/src/app/redirect/thunks.ts +++ b/commafeed-client/src/app/redirect/thunks.ts @@ -1,45 +1,45 @@ -import { createAppAsyncThunk } from "app/async-thunk" -import { Constants } from "app/constants" -import { redirectTo } from "app/redirect/slice" - -export const redirectToLogin = createAppAsyncThunk("redirect/login", (_, thunkApi) => thunkApi.dispatch(redirectTo("/login"))) -export const redirectToRegistration = createAppAsyncThunk("redirect/register", (_, thunkApi) => thunkApi.dispatch(redirectTo("/register"))) -export const redirectToPasswordRecovery = createAppAsyncThunk("redirect/passwordRecovery", (_, thunkApi) => - thunkApi.dispatch(redirectTo("/passwordRecovery")) -) -export const redirectToApiDocumentation = createAppAsyncThunk("redirect/api", (_, thunkApi) => thunkApi.dispatch(redirectTo("/api"))) - -export const redirectToSelectedSource = createAppAsyncThunk("redirect/selectedSource", (_, thunkApi) => { - const { source } = thunkApi.getState().entries - thunkApi.dispatch(redirectTo(`/app/${source.type}/${source.id}`)) -}) -export const redirectToCategory = createAppAsyncThunk("redirect/category", (id: string, thunkApi) => - thunkApi.dispatch(redirectTo(`/app/category/${id}`)) -) -export const redirectToRootCategory = createAppAsyncThunk( - "redirect/category/root", - async (_, thunkApi) => await thunkApi.dispatch(redirectToCategory(Constants.categories.all.id)) -) -export const redirectToCategoryDetails = createAppAsyncThunk("redirect/category/details", (id: string, thunkApi) => - thunkApi.dispatch(redirectTo(`/app/category/${id}/details`)) -) -export const redirectToFeed = createAppAsyncThunk("redirect/feed", (id: string | number, thunkApi) => - thunkApi.dispatch(redirectTo(`/app/feed/${id}`)) -) -export const redirectToFeedDetails = createAppAsyncThunk("redirect/feed/details", (id: string, thunkApi) => - thunkApi.dispatch(redirectTo(`/app/feed/${id}/details`)) -) -export const redirectToTag = createAppAsyncThunk("redirect/tag", (id: string, thunkApi) => thunkApi.dispatch(redirectTo(`/app/tag/${id}`))) -export const redirectToTagDetails = createAppAsyncThunk("redirect/tag/details", (id: string, thunkApi) => - thunkApi.dispatch(redirectTo(`/app/tag/${id}/details`)) -) -export const redirectToAdd = createAppAsyncThunk("redirect/add", (_, thunkApi) => thunkApi.dispatch(redirectTo("/app/add"))) -export const redirectToSettings = createAppAsyncThunk("redirect/settings", (_, thunkApi) => thunkApi.dispatch(redirectTo("/app/settings"))) -export const redirectToAdminUsers = createAppAsyncThunk("redirect/admin/users", (_, thunkApi) => - thunkApi.dispatch(redirectTo("/app/admin/users")) -) -export const redirectToMetrics = createAppAsyncThunk("redirect/admin/metrics", (_, thunkApi) => - thunkApi.dispatch(redirectTo("/app/admin/metrics")) -) -export const redirectToDonate = createAppAsyncThunk("redirect/donate", (_, thunkApi) => thunkApi.dispatch(redirectTo("/app/donate"))) -export const redirectToAbout = createAppAsyncThunk("redirect/about", (_, thunkApi) => thunkApi.dispatch(redirectTo("/app/about"))) +import { createAppAsyncThunk } from "app/async-thunk" +import { Constants } from "app/constants" +import { redirectTo } from "app/redirect/slice" + +export const redirectToLogin = createAppAsyncThunk("redirect/login", (_, thunkApi) => thunkApi.dispatch(redirectTo("/login"))) +export const redirectToRegistration = createAppAsyncThunk("redirect/register", (_, thunkApi) => thunkApi.dispatch(redirectTo("/register"))) +export const redirectToPasswordRecovery = createAppAsyncThunk("redirect/passwordRecovery", (_, thunkApi) => + thunkApi.dispatch(redirectTo("/passwordRecovery")) +) +export const redirectToApiDocumentation = createAppAsyncThunk("redirect/api", (_, thunkApi) => thunkApi.dispatch(redirectTo("/api"))) + +export const redirectToSelectedSource = createAppAsyncThunk("redirect/selectedSource", (_, thunkApi) => { + const { source } = thunkApi.getState().entries + thunkApi.dispatch(redirectTo(`/app/${source.type}/${source.id}`)) +}) +export const redirectToCategory = createAppAsyncThunk("redirect/category", (id: string, thunkApi) => + thunkApi.dispatch(redirectTo(`/app/category/${id}`)) +) +export const redirectToRootCategory = createAppAsyncThunk( + "redirect/category/root", + async (_, thunkApi) => await thunkApi.dispatch(redirectToCategory(Constants.categories.all.id)) +) +export const redirectToCategoryDetails = createAppAsyncThunk("redirect/category/details", (id: string, thunkApi) => + thunkApi.dispatch(redirectTo(`/app/category/${id}/details`)) +) +export const redirectToFeed = createAppAsyncThunk("redirect/feed", (id: string | number, thunkApi) => + thunkApi.dispatch(redirectTo(`/app/feed/${id}`)) +) +export const redirectToFeedDetails = createAppAsyncThunk("redirect/feed/details", (id: string, thunkApi) => + thunkApi.dispatch(redirectTo(`/app/feed/${id}/details`)) +) +export const redirectToTag = createAppAsyncThunk("redirect/tag", (id: string, thunkApi) => thunkApi.dispatch(redirectTo(`/app/tag/${id}`))) +export const redirectToTagDetails = createAppAsyncThunk("redirect/tag/details", (id: string, thunkApi) => + thunkApi.dispatch(redirectTo(`/app/tag/${id}/details`)) +) +export const redirectToAdd = createAppAsyncThunk("redirect/add", (_, thunkApi) => thunkApi.dispatch(redirectTo("/app/add"))) +export const redirectToSettings = createAppAsyncThunk("redirect/settings", (_, thunkApi) => thunkApi.dispatch(redirectTo("/app/settings"))) +export const redirectToAdminUsers = createAppAsyncThunk("redirect/admin/users", (_, thunkApi) => + thunkApi.dispatch(redirectTo("/app/admin/users")) +) +export const redirectToMetrics = createAppAsyncThunk("redirect/admin/metrics", (_, thunkApi) => + thunkApi.dispatch(redirectTo("/app/admin/metrics")) +) +export const redirectToDonate = createAppAsyncThunk("redirect/donate", (_, thunkApi) => thunkApi.dispatch(redirectTo("/app/donate"))) +export const redirectToAbout = createAppAsyncThunk("redirect/about", (_, thunkApi) => thunkApi.dispatch(redirectTo("/app/about"))) diff --git a/commafeed-client/src/app/server/slice.ts b/commafeed-client/src/app/server/slice.ts index 93ef61fb..954c6865 100644 --- a/commafeed-client/src/app/server/slice.ts +++ b/commafeed-client/src/app/server/slice.ts @@ -1,29 +1,29 @@ -import { createSlice, type PayloadAction } from "@reduxjs/toolkit" -import { reloadServerInfos } from "app/server/thunks" -import { type ServerInfo } from "app/types" - -interface ServerState { - serverInfos?: ServerInfo - webSocketConnected: boolean -} - -const initialState: ServerState = { - webSocketConnected: false, -} - -export const serverSlice = createSlice({ - name: "server", - initialState, - reducers: { - setWebSocketConnected: (state, action: PayloadAction) => { - state.webSocketConnected = action.payload - }, - }, - extraReducers: builder => { - builder.addCase(reloadServerInfos.fulfilled, (state, action) => { - state.serverInfos = action.payload - }) - }, -}) - -export const { setWebSocketConnected } = serverSlice.actions +import { type PayloadAction, createSlice } from "@reduxjs/toolkit" +import { reloadServerInfos } from "app/server/thunks" +import type { ServerInfo } from "app/types" + +interface ServerState { + serverInfos?: ServerInfo + webSocketConnected: boolean +} + +const initialState: ServerState = { + webSocketConnected: false, +} + +export const serverSlice = createSlice({ + name: "server", + initialState, + reducers: { + setWebSocketConnected: (state, action: PayloadAction) => { + state.webSocketConnected = action.payload + }, + }, + extraReducers: builder => { + builder.addCase(reloadServerInfos.fulfilled, (state, action) => { + state.serverInfos = action.payload + }) + }, +}) + +export const { setWebSocketConnected } = serverSlice.actions diff --git a/commafeed-client/src/app/server/thunks.ts b/commafeed-client/src/app/server/thunks.ts index 6d4c8200..6fa1c12e 100644 --- a/commafeed-client/src/app/server/thunks.ts +++ b/commafeed-client/src/app/server/thunks.ts @@ -1,4 +1,4 @@ -import { createAppAsyncThunk } from "app/async-thunk" -import { client } from "app/client" - -export const reloadServerInfos = createAppAsyncThunk("server/infos", async () => await client.server.getServerInfos().then(r => r.data)) +import { createAppAsyncThunk } from "app/async-thunk" +import { client } from "app/client" + +export const reloadServerInfos = createAppAsyncThunk("server/infos", async () => await client.server.getServerInfos().then(r => r.data)) diff --git a/commafeed-client/src/app/store.ts b/commafeed-client/src/app/store.ts index e7601c2c..a4eb1f4c 100644 --- a/commafeed-client/src/app/store.ts +++ b/commafeed-client/src/app/store.ts @@ -1,23 +1,23 @@ -import { configureStore } from "@reduxjs/toolkit" -import { entriesSlice } from "app/entries/slice" -import { redirectSlice } from "app/redirect/slice" -import { serverSlice } from "app/server/slice" -import { treeSlice } from "app/tree/slice" -import { userSlice } from "app/user/slice" -import { type TypedUseSelectorHook, useDispatch, useSelector } from "react-redux" - -export const reducers = { - entries: entriesSlice.reducer, - redirect: redirectSlice.reducer, - tree: treeSlice.reducer, - server: serverSlice.reducer, - user: userSlice.reducer, -} - -export const store = configureStore({ reducer: reducers }) - -export type RootState = ReturnType -export type AppDispatch = typeof store.dispatch - -export const useAppDispatch: () => AppDispatch = useDispatch -export const useAppSelector: TypedUseSelectorHook = useSelector +import { configureStore } from "@reduxjs/toolkit" +import { entriesSlice } from "app/entries/slice" +import { redirectSlice } from "app/redirect/slice" +import { serverSlice } from "app/server/slice" +import { treeSlice } from "app/tree/slice" +import { userSlice } from "app/user/slice" +import { type TypedUseSelectorHook, useDispatch, useSelector } from "react-redux" + +export const reducers = { + entries: entriesSlice.reducer, + redirect: redirectSlice.reducer, + tree: treeSlice.reducer, + server: serverSlice.reducer, + user: userSlice.reducer, +} + +export const store = configureStore({ reducer: reducers }) + +export type RootState = ReturnType +export type AppDispatch = typeof store.dispatch + +export const useAppDispatch: () => AppDispatch = useDispatch +export const useAppSelector: TypedUseSelectorHook = useSelector diff --git a/commafeed-client/src/app/tree/slice.ts b/commafeed-client/src/app/tree/slice.ts index 0c386525..fd863350 100644 --- a/commafeed-client/src/app/tree/slice.ts +++ b/commafeed-client/src/app/tree/slice.ts @@ -1,72 +1,68 @@ -import { createSlice, type PayloadAction } from "@reduxjs/toolkit" -import { markEntry } from "app/entries/thunks" -import { redirectTo } from "app/redirect/slice" -import { collapseTreeCategory, reloadTree } from "app/tree/thunks" -import { type Category } from "app/types" -import { visitCategoryTree } from "app/utils" - -interface TreeState { - rootCategory?: Category - mobileMenuOpen: boolean - sidebarVisible: boolean -} - -const initialState: TreeState = { - mobileMenuOpen: false, - sidebarVisible: true, -} - -export const treeSlice = createSlice({ - name: "tree", - initialState, - reducers: { - setMobileMenuOpen: (state, action: PayloadAction) => { - state.mobileMenuOpen = action.payload - }, - toggleSidebar: state => { - state.sidebarVisible = !state.sidebarVisible - }, - incrementUnreadCount: ( - state, - action: PayloadAction<{ - feedId: number - amount: number - }> - ) => { - if (!state.rootCategory) return - visitCategoryTree(state.rootCategory, c => - c.feeds - .filter(f => f.id === action.payload.feedId) - .forEach(f => { - f.unread += action.payload.amount - }) - ) - }, - }, - extraReducers: builder => { - builder.addCase(reloadTree.fulfilled, (state, action) => { - state.rootCategory = action.payload - }) - builder.addCase(collapseTreeCategory.pending, (state, action) => { - if (!state.rootCategory) return - visitCategoryTree(state.rootCategory, c => { - if (+c.id === action.meta.arg.id) c.expanded = !action.meta.arg.collapse - }) - }) - builder.addCase(markEntry.pending, (state, action) => { - if (!state.rootCategory) return - visitCategoryTree(state.rootCategory, c => - c.feeds - .filter(f => f.id === +action.meta.arg.entry.feedId) - .forEach(f => { - f.unread = action.meta.arg.read ? f.unread - 1 : f.unread + 1 - }) - ) - }) - builder.addCase(redirectTo, state => { - state.mobileMenuOpen = false - }) - }, -}) - -export const { setMobileMenuOpen, toggleSidebar, incrementUnreadCount } = treeSlice.actions +import { type PayloadAction, createSlice } from "@reduxjs/toolkit" +import { markEntry } from "app/entries/thunks" +import { redirectTo } from "app/redirect/slice" +import { collapseTreeCategory, reloadTree } from "app/tree/thunks" +import type { Category } from "app/types" +import { visitCategoryTree } from "app/utils" + +interface TreeState { + rootCategory?: Category + mobileMenuOpen: boolean + sidebarVisible: boolean +} + +const initialState: TreeState = { + mobileMenuOpen: false, + sidebarVisible: true, +} + +export const treeSlice = createSlice({ + name: "tree", + initialState, + reducers: { + setMobileMenuOpen: (state, action: PayloadAction) => { + state.mobileMenuOpen = action.payload + }, + toggleSidebar: state => { + state.sidebarVisible = !state.sidebarVisible + }, + incrementUnreadCount: ( + state, + action: PayloadAction<{ + feedId: number + amount: number + }> + ) => { + if (!state.rootCategory) return + visitCategoryTree(state.rootCategory, c => { + for (const f of c.feeds.filter(f => f.id === action.payload.feedId)) { + f.unread += action.payload.amount + } + }) + }, + }, + extraReducers: builder => { + builder.addCase(reloadTree.fulfilled, (state, action) => { + state.rootCategory = action.payload + }) + builder.addCase(collapseTreeCategory.pending, (state, action) => { + if (!state.rootCategory) return + visitCategoryTree(state.rootCategory, c => { + if (+c.id === action.meta.arg.id) c.expanded = !action.meta.arg.collapse + }) + }) + builder.addCase(markEntry.pending, (state, action) => { + if (!state.rootCategory) return + visitCategoryTree(state.rootCategory, c => { + for (const f of c.feeds.filter(f => f.id === +action.meta.arg.entry.feedId)) { + f.unread = action.meta.arg.read ? f.unread - 1 : f.unread + 1 + } + }) + }) + builder.addCase(redirectTo, state => { + state.mobileMenuOpen = false + }) + }, +}) + +export const { setMobileMenuOpen, toggleSidebar, incrementUnreadCount } = treeSlice.actions diff --git a/commafeed-client/src/app/tree/thunks.ts b/commafeed-client/src/app/tree/thunks.ts index 4d9f9f33..32f6f185 100644 --- a/commafeed-client/src/app/tree/thunks.ts +++ b/commafeed-client/src/app/tree/thunks.ts @@ -1,9 +1,9 @@ -import { createAppAsyncThunk } from "app/async-thunk" -import { client } from "app/client" -import type { CollapseRequest } from "app/types" - -export const reloadTree = createAppAsyncThunk("tree/reload", async () => await client.category.getRoot().then(r => r.data)) -export const collapseTreeCategory = createAppAsyncThunk( - "tree/category/collapse", - async (req: CollapseRequest) => await client.category.collapse(req) -) +import { createAppAsyncThunk } from "app/async-thunk" +import { client } from "app/client" +import type { CollapseRequest } from "app/types" + +export const reloadTree = createAppAsyncThunk("tree/reload", async () => await client.category.getRoot().then(r => r.data)) +export const collapseTreeCategory = createAppAsyncThunk( + "tree/category/collapse", + async (req: CollapseRequest) => await client.category.collapse(req) +) diff --git a/commafeed-client/src/app/types.ts b/commafeed-client/src/app/types.ts index e782deee..f27c944c 100644 --- a/commafeed-client/src/app/types.ts +++ b/commafeed-client/src/app/types.ts @@ -1,296 +1,296 @@ -export type ReadingMode = "all" | "unread" - -export type ReadingOrder = "asc" | "desc" - -export type ViewMode = "title" | "cozy" | "detailed" | "expanded" - -export type ScrollMode = "always" | "never" | "if_needed" - -export type IconDisplayMode = "always" | "never" | "on_desktop" | "on_mobile" - -export interface AddCategoryRequest { - name: string - parentId?: string -} - -export interface Subscription { - id: number - name: string - message?: string - errorCount: number - lastRefresh?: number - nextRefresh?: number - feedUrl: string - feedLink: string - iconUrl: string - unread: number - categoryId?: string - position: number - newestItemTime?: number - filter?: string -} - -export interface Category { - id: string - parentId?: string - parentName?: string - name: string - children: Category[] - feeds: Subscription[] - expanded: boolean - position: number -} - -export interface CategoryModificationRequest { - id: number - name?: string - parentId?: string - position?: number -} - -export interface CollapseRequest { - id: number - collapse: boolean -} - -export interface Entry { - id: string - guid: string - title: string - content: string - categories?: string - rtl: boolean - author?: string - enclosureUrl?: string - enclosureType?: string - mediaDescription?: string - mediaThumbnailUrl?: string - mediaThumbnailWidth?: number - mediaThumbnailHeight?: number - date: number - insertedDate: number - feedId: string - feedName: string - feedUrl: string - feedLink: string - iconUrl: string - url: string - read: boolean - starred: boolean - markable: boolean - tags: string[] -} - -export interface Entries { - name: string - message?: string - errorCount: number - feedLink: string - timestamp: number - hasMore: boolean - offset?: number - limit?: number - entries: Entry[] - ignoredReadStatus: boolean -} - -export interface FeedInfo { - url: string - title: string -} - -export interface FeedInfoRequest { - url: string -} - -export interface FeedModificationRequest { - id: number - name?: string - categoryId?: string - position?: number - filter?: string -} - -export interface GetEntriesRequest { - id: string - readType?: ReadingMode - newerThan?: number - order?: ReadingOrder - keywords?: string - onlyIds?: boolean - excludedSubscriptionIds?: string - tag?: string -} - -export interface GetEntriesPaginatedRequest extends GetEntriesRequest { - offset: number - limit: number -} - -export interface IDRequest { - id: number -} - -export interface LoginRequest { - name: string - password: string -} - -export interface MarkRequest { - id: string - read: boolean - olderThan?: number - insertedBefore?: number - keywords?: string - excludedSubscriptions?: number[] -} - -export interface MetricCounter { - count: number -} - -export interface MetricGauge { - value: number -} - -export interface MetricMeter { - count: number - m15_rate: number - m1_rate: number - m5_rate: number - mean_rate: number - units: string -} - -export interface MetricTimer { - count: number - max: number - mean: number - min: number - p50: number - p75: number - p95: number - p98: number - p99: number - p999: number - stddev: number - m15_rate: number - m1_rate: number - m5_rate: number - mean_rate: number - duration_units: string - rate_units: string -} - -export interface Metrics { - counters: Record - gauges: Record - meters: Record - timers: Record -} - -export interface MultipleMarkRequest { - requests: MarkRequest[] -} - -export interface PasswordResetRequest { - email: string -} - -export interface ProfileModificationRequest { - currentPassword: string - email: string - newPassword?: string - newApiKey?: boolean -} - -export interface RegistrationRequest { - name: string - password: string - email: string -} - -export interface ServerInfo { - announcement?: string - version: string - gitCommit: string - allowRegistrations: boolean - googleAnalyticsCode?: string - smtpEnabled: boolean - demoAccountEnabled: boolean - websocketEnabled: boolean - websocketPingInterval: number - treeReloadInterval: number -} - -export interface SharingSettings { - email: boolean - gmail: boolean - facebook: boolean - twitter: boolean - tumblr: boolean - pocket: boolean - instapaper: boolean - buffer: boolean -} - -export interface Settings { - language: string - readingMode: ReadingMode - readingOrder: ReadingOrder - showRead: boolean - scrollMarks: boolean - customCss?: string - customJs?: string - scrollSpeed: number - scrollMode: ScrollMode - starIconDisplayMode: IconDisplayMode - externalLinkIconDisplayMode: IconDisplayMode - markAllAsReadConfirmation: boolean - customContextMenu: boolean - mobileFooter: boolean - sharingSettings: SharingSettings -} - -export interface StarRequest { - id: string - feedId: number - starred: boolean -} - -export interface SubscribeRequest { - url: string - title: string - categoryId?: string -} - -export interface TagRequest { - entryId: number - tags: string[] -} - -export interface UserModel { - id: number - name: string - email?: string - apiKey?: string - password?: string - enabled: boolean - created: number - lastLogin?: number - admin: boolean -} - -export interface AdminSaveUserRequest { - id?: number - name: string - email?: string - password?: string - enabled: boolean - admin: boolean -} - -export interface AuthenticationError { - message: string - allowRegistrations: boolean -} +export type ReadingMode = "all" | "unread" + +export type ReadingOrder = "asc" | "desc" + +export type ViewMode = "title" | "cozy" | "detailed" | "expanded" + +export type ScrollMode = "always" | "never" | "if_needed" + +export type IconDisplayMode = "always" | "never" | "on_desktop" | "on_mobile" + +export interface AddCategoryRequest { + name: string + parentId?: string +} + +export interface Subscription { + id: number + name: string + message?: string + errorCount: number + lastRefresh?: number + nextRefresh?: number + feedUrl: string + feedLink: string + iconUrl: string + unread: number + categoryId?: string + position: number + newestItemTime?: number + filter?: string +} + +export interface Category { + id: string + parentId?: string + parentName?: string + name: string + children: Category[] + feeds: Subscription[] + expanded: boolean + position: number +} + +export interface CategoryModificationRequest { + id: number + name?: string + parentId?: string + position?: number +} + +export interface CollapseRequest { + id: number + collapse: boolean +} + +export interface Entry { + id: string + guid: string + title: string + content: string + categories?: string + rtl: boolean + author?: string + enclosureUrl?: string + enclosureType?: string + mediaDescription?: string + mediaThumbnailUrl?: string + mediaThumbnailWidth?: number + mediaThumbnailHeight?: number + date: number + insertedDate: number + feedId: string + feedName: string + feedUrl: string + feedLink: string + iconUrl: string + url: string + read: boolean + starred: boolean + markable: boolean + tags: string[] +} + +export interface Entries { + name: string + message?: string + errorCount: number + feedLink: string + timestamp: number + hasMore: boolean + offset?: number + limit?: number + entries: Entry[] + ignoredReadStatus: boolean +} + +export interface FeedInfo { + url: string + title: string +} + +export interface FeedInfoRequest { + url: string +} + +export interface FeedModificationRequest { + id: number + name?: string + categoryId?: string + position?: number + filter?: string +} + +export interface GetEntriesRequest { + id: string + readType?: ReadingMode + newerThan?: number + order?: ReadingOrder + keywords?: string + onlyIds?: boolean + excludedSubscriptionIds?: string + tag?: string +} + +export interface GetEntriesPaginatedRequest extends GetEntriesRequest { + offset: number + limit: number +} + +export interface IDRequest { + id: number +} + +export interface LoginRequest { + name: string + password: string +} + +export interface MarkRequest { + id: string + read: boolean + olderThan?: number + insertedBefore?: number + keywords?: string + excludedSubscriptions?: number[] +} + +export interface MetricCounter { + count: number +} + +export interface MetricGauge { + value: number +} + +export interface MetricMeter { + count: number + m15_rate: number + m1_rate: number + m5_rate: number + mean_rate: number + units: string +} + +export interface MetricTimer { + count: number + max: number + mean: number + min: number + p50: number + p75: number + p95: number + p98: number + p99: number + p999: number + stddev: number + m15_rate: number + m1_rate: number + m5_rate: number + mean_rate: number + duration_units: string + rate_units: string +} + +export interface Metrics { + counters: Record + gauges: Record + meters: Record + timers: Record +} + +export interface MultipleMarkRequest { + requests: MarkRequest[] +} + +export interface PasswordResetRequest { + email: string +} + +export interface ProfileModificationRequest { + currentPassword: string + email: string + newPassword?: string + newApiKey?: boolean +} + +export interface RegistrationRequest { + name: string + password: string + email: string +} + +export interface ServerInfo { + announcement?: string + version: string + gitCommit: string + allowRegistrations: boolean + googleAnalyticsCode?: string + smtpEnabled: boolean + demoAccountEnabled: boolean + websocketEnabled: boolean + websocketPingInterval: number + treeReloadInterval: number +} + +export interface SharingSettings { + email: boolean + gmail: boolean + facebook: boolean + twitter: boolean + tumblr: boolean + pocket: boolean + instapaper: boolean + buffer: boolean +} + +export interface Settings { + language: string + readingMode: ReadingMode + readingOrder: ReadingOrder + showRead: boolean + scrollMarks: boolean + customCss?: string + customJs?: string + scrollSpeed: number + scrollMode: ScrollMode + starIconDisplayMode: IconDisplayMode + externalLinkIconDisplayMode: IconDisplayMode + markAllAsReadConfirmation: boolean + customContextMenu: boolean + mobileFooter: boolean + sharingSettings: SharingSettings +} + +export interface StarRequest { + id: string + feedId: number + starred: boolean +} + +export interface SubscribeRequest { + url: string + title: string + categoryId?: string +} + +export interface TagRequest { + entryId: number + tags: string[] +} + +export interface UserModel { + id: number + name: string + email?: string + apiKey?: string + password?: string + enabled: boolean + created: number + lastLogin?: number + admin: boolean +} + +export interface AdminSaveUserRequest { + id?: number + name: string + email?: string + password?: string + enabled: boolean + admin: boolean +} + +export interface AuthenticationError { + message: string + allowRegistrations: boolean +} diff --git a/commafeed-client/src/app/user/slice.ts b/commafeed-client/src/app/user/slice.ts index 0f125ea7..f1611355 100644 --- a/commafeed-client/src/app/user/slice.ts +++ b/commafeed-client/src/app/user/slice.ts @@ -1,120 +1,120 @@ -import { t } from "@lingui/macro" -import { showNotification } from "@mantine/notifications" -import { createSlice, isAnyOf } from "@reduxjs/toolkit" -import { type Settings, type UserModel } from "app/types" -import { - changeCustomContextMenu, - changeExternalLinkIconDisplayMode, - changeLanguage, - changeMarkAllAsReadConfirmation, - changeMobileFooter, - changeReadingMode, - changeReadingOrder, - changeScrollMarks, - changeScrollMode, - changeScrollSpeed, - changeSharingSetting, - changeShowRead, - changeStarIconDisplayMode, - reloadProfile, - reloadSettings, - reloadTags, -} from "./thunks" - -interface UserState { - settings?: Settings - profile?: UserModel - tags?: string[] -} - -const initialState: UserState = {} - -export const userSlice = createSlice({ - name: "user", - initialState, - reducers: {}, - extraReducers: builder => { - builder.addCase(reloadSettings.fulfilled, (state, action) => { - state.settings = action.payload - }) - builder.addCase(reloadProfile.fulfilled, (state, action) => { - state.profile = action.payload - }) - builder.addCase(reloadTags.fulfilled, (state, action) => { - state.tags = action.payload - }) - builder.addCase(changeReadingMode.pending, (state, action) => { - if (!state.settings) return - state.settings.readingMode = action.meta.arg - }) - builder.addCase(changeReadingOrder.pending, (state, action) => { - if (!state.settings) return - state.settings.readingOrder = action.meta.arg - }) - builder.addCase(changeLanguage.pending, (state, action) => { - if (!state.settings) return - state.settings.language = action.meta.arg - }) - builder.addCase(changeScrollSpeed.pending, (state, action) => { - if (!state.settings) return - state.settings.scrollSpeed = action.meta.arg ? 400 : 0 - }) - builder.addCase(changeShowRead.pending, (state, action) => { - if (!state.settings) return - state.settings.showRead = action.meta.arg - }) - builder.addCase(changeScrollMarks.pending, (state, action) => { - if (!state.settings) return - state.settings.scrollMarks = action.meta.arg - }) - builder.addCase(changeScrollMode.pending, (state, action) => { - if (!state.settings) return - state.settings.scrollMode = action.meta.arg - }) - builder.addCase(changeStarIconDisplayMode.pending, (state, action) => { - if (!state.settings) return - state.settings.starIconDisplayMode = action.meta.arg - }) - builder.addCase(changeExternalLinkIconDisplayMode.pending, (state, action) => { - if (!state.settings) return - state.settings.externalLinkIconDisplayMode = action.meta.arg - }) - builder.addCase(changeMarkAllAsReadConfirmation.pending, (state, action) => { - if (!state.settings) return - state.settings.markAllAsReadConfirmation = action.meta.arg - }) - builder.addCase(changeCustomContextMenu.pending, (state, action) => { - if (!state.settings) return - state.settings.customContextMenu = action.meta.arg - }) - builder.addCase(changeMobileFooter.pending, (state, action) => { - if (!state.settings) return - state.settings.mobileFooter = action.meta.arg - }) - builder.addCase(changeSharingSetting.pending, (state, action) => { - if (!state.settings) return - state.settings.sharingSettings[action.meta.arg.site] = action.meta.arg.value - }) - builder.addMatcher( - isAnyOf( - changeLanguage.fulfilled, - changeScrollSpeed.fulfilled, - changeShowRead.fulfilled, - changeScrollMarks.fulfilled, - changeScrollMode.fulfilled, - changeStarIconDisplayMode.fulfilled, - changeExternalLinkIconDisplayMode.fulfilled, - changeMarkAllAsReadConfirmation.fulfilled, - changeCustomContextMenu.fulfilled, - changeMobileFooter.fulfilled, - changeSharingSetting.fulfilled - ), - () => { - showNotification({ - message: t`Settings saved.`, - color: "green", - }) - } - ) - }, -}) +import { t } from "@lingui/macro" +import { showNotification } from "@mantine/notifications" +import { createSlice, isAnyOf } from "@reduxjs/toolkit" +import type { Settings, UserModel } from "app/types" +import { + changeCustomContextMenu, + changeExternalLinkIconDisplayMode, + changeLanguage, + changeMarkAllAsReadConfirmation, + changeMobileFooter, + changeReadingMode, + changeReadingOrder, + changeScrollMarks, + changeScrollMode, + changeScrollSpeed, + changeSharingSetting, + changeShowRead, + changeStarIconDisplayMode, + reloadProfile, + reloadSettings, + reloadTags, +} from "./thunks" + +interface UserState { + settings?: Settings + profile?: UserModel + tags?: string[] +} + +const initialState: UserState = {} + +export const userSlice = createSlice({ + name: "user", + initialState, + reducers: {}, + extraReducers: builder => { + builder.addCase(reloadSettings.fulfilled, (state, action) => { + state.settings = action.payload + }) + builder.addCase(reloadProfile.fulfilled, (state, action) => { + state.profile = action.payload + }) + builder.addCase(reloadTags.fulfilled, (state, action) => { + state.tags = action.payload + }) + builder.addCase(changeReadingMode.pending, (state, action) => { + if (!state.settings) return + state.settings.readingMode = action.meta.arg + }) + builder.addCase(changeReadingOrder.pending, (state, action) => { + if (!state.settings) return + state.settings.readingOrder = action.meta.arg + }) + builder.addCase(changeLanguage.pending, (state, action) => { + if (!state.settings) return + state.settings.language = action.meta.arg + }) + builder.addCase(changeScrollSpeed.pending, (state, action) => { + if (!state.settings) return + state.settings.scrollSpeed = action.meta.arg ? 400 : 0 + }) + builder.addCase(changeShowRead.pending, (state, action) => { + if (!state.settings) return + state.settings.showRead = action.meta.arg + }) + builder.addCase(changeScrollMarks.pending, (state, action) => { + if (!state.settings) return + state.settings.scrollMarks = action.meta.arg + }) + builder.addCase(changeScrollMode.pending, (state, action) => { + if (!state.settings) return + state.settings.scrollMode = action.meta.arg + }) + builder.addCase(changeStarIconDisplayMode.pending, (state, action) => { + if (!state.settings) return + state.settings.starIconDisplayMode = action.meta.arg + }) + builder.addCase(changeExternalLinkIconDisplayMode.pending, (state, action) => { + if (!state.settings) return + state.settings.externalLinkIconDisplayMode = action.meta.arg + }) + builder.addCase(changeMarkAllAsReadConfirmation.pending, (state, action) => { + if (!state.settings) return + state.settings.markAllAsReadConfirmation = action.meta.arg + }) + builder.addCase(changeCustomContextMenu.pending, (state, action) => { + if (!state.settings) return + state.settings.customContextMenu = action.meta.arg + }) + builder.addCase(changeMobileFooter.pending, (state, action) => { + if (!state.settings) return + state.settings.mobileFooter = action.meta.arg + }) + builder.addCase(changeSharingSetting.pending, (state, action) => { + if (!state.settings) return + state.settings.sharingSettings[action.meta.arg.site] = action.meta.arg.value + }) + builder.addMatcher( + isAnyOf( + changeLanguage.fulfilled, + changeScrollSpeed.fulfilled, + changeShowRead.fulfilled, + changeScrollMarks.fulfilled, + changeScrollMode.fulfilled, + changeStarIconDisplayMode.fulfilled, + changeExternalLinkIconDisplayMode.fulfilled, + changeMarkAllAsReadConfirmation.fulfilled, + changeCustomContextMenu.fulfilled, + changeMobileFooter.fulfilled, + changeSharingSetting.fulfilled + ), + () => { + showNotification({ + message: t`Settings saved.`, + color: "green", + }) + } + ) + }, +}) diff --git a/commafeed-client/src/app/user/thunks.ts b/commafeed-client/src/app/user/thunks.ts index 12c2a553..dd88bd0e 100644 --- a/commafeed-client/src/app/user/thunks.ts +++ b/commafeed-client/src/app/user/thunks.ts @@ -1,99 +1,99 @@ -import { createAppAsyncThunk } from "app/async-thunk" -import { client } from "app/client" -import { reloadEntries } from "app/entries/thunks" -import type { IconDisplayMode, ReadingMode, ReadingOrder, ScrollMode, SharingSettings } from "app/types" - -export const reloadSettings = createAppAsyncThunk("settings/reload", async () => await client.user.getSettings().then(r => r.data)) -export const reloadProfile = createAppAsyncThunk("profile/reload", async () => await client.user.getProfile().then(r => r.data)) -export const reloadTags = createAppAsyncThunk("entries/tags", async () => await client.entry.getTags().then(r => r.data)) -export const changeReadingMode = createAppAsyncThunk("settings/readingMode", (readingMode: ReadingMode, thunkApi) => { - const { settings } = thunkApi.getState().user - if (!settings) return - client.user.saveSettings({ ...settings, readingMode }) - thunkApi.dispatch(reloadEntries()) -}) -export const changeReadingOrder = createAppAsyncThunk("settings/readingOrder", (readingOrder: ReadingOrder, thunkApi) => { - const { settings } = thunkApi.getState().user - if (!settings) return - client.user.saveSettings({ ...settings, readingOrder }) - thunkApi.dispatch(reloadEntries()) -}) -export const changeLanguage = createAppAsyncThunk("settings/language", (language: string, thunkApi) => { - const { settings } = thunkApi.getState().user - if (!settings) return - client.user.saveSettings({ ...settings, language }) -}) -export const changeScrollSpeed = createAppAsyncThunk("settings/scrollSpeed", (speed: boolean, thunkApi) => { - const { settings } = thunkApi.getState().user - if (!settings) return - client.user.saveSettings({ ...settings, scrollSpeed: speed ? 400 : 0 }) -}) -export const changeShowRead = createAppAsyncThunk("settings/showRead", (showRead: boolean, thunkApi) => { - const { settings } = thunkApi.getState().user - if (!settings) return - client.user.saveSettings({ ...settings, showRead }) -}) -export const changeScrollMarks = createAppAsyncThunk("settings/scrollMarks", (scrollMarks: boolean, thunkApi) => { - const { settings } = thunkApi.getState().user - if (!settings) return - client.user.saveSettings({ ...settings, scrollMarks }) -}) -export const changeScrollMode = createAppAsyncThunk("settings/scrollMode", (scrollMode: ScrollMode, thunkApi) => { - const { settings } = thunkApi.getState().user - if (!settings) return - client.user.saveSettings({ ...settings, scrollMode }) -}) -export const changeStarIconDisplayMode = createAppAsyncThunk( - "settings/starIconDisplayMode", - (starIconDisplayMode: IconDisplayMode, thunkApi) => { - const { settings } = thunkApi.getState().user - if (!settings) return - client.user.saveSettings({ ...settings, starIconDisplayMode }) - } -) -export const changeExternalLinkIconDisplayMode = createAppAsyncThunk( - "settings/externalLinkIconDisplayMode", - (externalLinkIconDisplayMode: IconDisplayMode, thunkApi) => { - const { settings } = thunkApi.getState().user - if (!settings) return - client.user.saveSettings({ ...settings, externalLinkIconDisplayMode }) - } -) -export const changeMarkAllAsReadConfirmation = createAppAsyncThunk( - "settings/markAllAsReadConfirmation", - (markAllAsReadConfirmation: boolean, thunkApi) => { - const { settings } = thunkApi.getState().user - if (!settings) return - client.user.saveSettings({ ...settings, markAllAsReadConfirmation }) - } -) -export const changeCustomContextMenu = createAppAsyncThunk("settings/customContextMenu", (customContextMenu: boolean, thunkApi) => { - const { settings } = thunkApi.getState().user - if (!settings) return - client.user.saveSettings({ ...settings, customContextMenu }) -}) -export const changeMobileFooter = createAppAsyncThunk("settings/mobileFooter", (mobileFooter: boolean, thunkApi) => { - const { settings } = thunkApi.getState().user - if (!settings) return - client.user.saveSettings({ ...settings, mobileFooter }) -}) -export const changeSharingSetting = createAppAsyncThunk( - "settings/sharingSetting", - ( - sharingSetting: { - site: keyof SharingSettings - value: boolean - }, - thunkApi - ) => { - const { settings } = thunkApi.getState().user - if (!settings) return - client.user.saveSettings({ - ...settings, - sharingSettings: { - ...settings.sharingSettings, - [sharingSetting.site]: sharingSetting.value, - }, - }) - } -) +import { createAppAsyncThunk } from "app/async-thunk" +import { client } from "app/client" +import { reloadEntries } from "app/entries/thunks" +import type { IconDisplayMode, ReadingMode, ReadingOrder, ScrollMode, SharingSettings } from "app/types" + +export const reloadSettings = createAppAsyncThunk("settings/reload", async () => await client.user.getSettings().then(r => r.data)) +export const reloadProfile = createAppAsyncThunk("profile/reload", async () => await client.user.getProfile().then(r => r.data)) +export const reloadTags = createAppAsyncThunk("entries/tags", async () => await client.entry.getTags().then(r => r.data)) +export const changeReadingMode = createAppAsyncThunk("settings/readingMode", (readingMode: ReadingMode, thunkApi) => { + const { settings } = thunkApi.getState().user + if (!settings) return + client.user.saveSettings({ ...settings, readingMode }) + thunkApi.dispatch(reloadEntries()) +}) +export const changeReadingOrder = createAppAsyncThunk("settings/readingOrder", (readingOrder: ReadingOrder, thunkApi) => { + const { settings } = thunkApi.getState().user + if (!settings) return + client.user.saveSettings({ ...settings, readingOrder }) + thunkApi.dispatch(reloadEntries()) +}) +export const changeLanguage = createAppAsyncThunk("settings/language", (language: string, thunkApi) => { + const { settings } = thunkApi.getState().user + if (!settings) return + client.user.saveSettings({ ...settings, language }) +}) +export const changeScrollSpeed = createAppAsyncThunk("settings/scrollSpeed", (speed: boolean, thunkApi) => { + const { settings } = thunkApi.getState().user + if (!settings) return + client.user.saveSettings({ ...settings, scrollSpeed: speed ? 400 : 0 }) +}) +export const changeShowRead = createAppAsyncThunk("settings/showRead", (showRead: boolean, thunkApi) => { + const { settings } = thunkApi.getState().user + if (!settings) return + client.user.saveSettings({ ...settings, showRead }) +}) +export const changeScrollMarks = createAppAsyncThunk("settings/scrollMarks", (scrollMarks: boolean, thunkApi) => { + const { settings } = thunkApi.getState().user + if (!settings) return + client.user.saveSettings({ ...settings, scrollMarks }) +}) +export const changeScrollMode = createAppAsyncThunk("settings/scrollMode", (scrollMode: ScrollMode, thunkApi) => { + const { settings } = thunkApi.getState().user + if (!settings) return + client.user.saveSettings({ ...settings, scrollMode }) +}) +export const changeStarIconDisplayMode = createAppAsyncThunk( + "settings/starIconDisplayMode", + (starIconDisplayMode: IconDisplayMode, thunkApi) => { + const { settings } = thunkApi.getState().user + if (!settings) return + client.user.saveSettings({ ...settings, starIconDisplayMode }) + } +) +export const changeExternalLinkIconDisplayMode = createAppAsyncThunk( + "settings/externalLinkIconDisplayMode", + (externalLinkIconDisplayMode: IconDisplayMode, thunkApi) => { + const { settings } = thunkApi.getState().user + if (!settings) return + client.user.saveSettings({ ...settings, externalLinkIconDisplayMode }) + } +) +export const changeMarkAllAsReadConfirmation = createAppAsyncThunk( + "settings/markAllAsReadConfirmation", + (markAllAsReadConfirmation: boolean, thunkApi) => { + const { settings } = thunkApi.getState().user + if (!settings) return + client.user.saveSettings({ ...settings, markAllAsReadConfirmation }) + } +) +export const changeCustomContextMenu = createAppAsyncThunk("settings/customContextMenu", (customContextMenu: boolean, thunkApi) => { + const { settings } = thunkApi.getState().user + if (!settings) return + client.user.saveSettings({ ...settings, customContextMenu }) +}) +export const changeMobileFooter = createAppAsyncThunk("settings/mobileFooter", (mobileFooter: boolean, thunkApi) => { + const { settings } = thunkApi.getState().user + if (!settings) return + client.user.saveSettings({ ...settings, mobileFooter }) +}) +export const changeSharingSetting = createAppAsyncThunk( + "settings/sharingSetting", + ( + sharingSetting: { + site: keyof SharingSettings + value: boolean + }, + thunkApi + ) => { + const { settings } = thunkApi.getState().user + if (!settings) return + client.user.saveSettings({ + ...settings, + sharingSettings: { + ...settings.sharingSettings, + [sharingSetting.site]: sharingSetting.value, + }, + }) + } +) diff --git a/commafeed-client/src/app/utils.ts b/commafeed-client/src/app/utils.ts index baed8e8d..b0f98b47 100644 --- a/commafeed-client/src/app/utils.ts +++ b/commafeed-client/src/app/utils.ts @@ -1,47 +1,49 @@ -import { throttle } from "throttle-debounce" -import { type Category } from "./types" - -export function visitCategoryTree(category: Category, visitor: (category: Category) => void): void { - visitor(category) - category.children.forEach(child => visitCategoryTree(child, visitor)) -} - -export function flattenCategoryTree(category: Category): Category[] { - const categories: Category[] = [] - visitCategoryTree(category, c => categories.push(c)) - return categories -} - -export function categoryUnreadCount(category?: Category): number { - if (!category) return 0 - - return flattenCategoryTree(category) - .flatMap(c => c.feeds) - .map(f => f.unread) - .reduce((total, current) => total + current, 0) -} - -export const calculatePlaceholderSize = ({ width, height, maxWidth }: { width?: number; height?: number; maxWidth: number }) => { - const placeholderWidth = width && Math.min(width, maxWidth) - const placeholderHeight = height && width && width > maxWidth ? height * (maxWidth / width) : height - return { width: placeholderWidth, height: placeholderHeight } -} - -export const scrollToWithCallback = ({ options, onScrollEnded }: { options: ScrollToOptions; onScrollEnded: () => void }) => { - const offset = (options.top ?? 0).toFixed() - - const onScroll = throttle(100, () => { - if (window.scrollY.toFixed() === offset) { - window.removeEventListener("scroll", onScroll) - onScrollEnded() - } - }) - window.addEventListener("scroll", onScroll) - - // scrollTo does not trigger if there's nothing to do, trigger it manually - onScroll() - - window.scrollTo(options) -} - -export const truncate = (str: string, n: number) => (str.length > n ? `${str.slice(0, n - 1)}\u2026` : str) +import { throttle } from "throttle-debounce" +import type { Category } from "./types" + +export function visitCategoryTree(category: Category, visitor: (category: Category) => void): void { + visitor(category) + for (const child of category.children) { + visitCategoryTree(child, visitor) + } +} + +export function flattenCategoryTree(category: Category): Category[] { + const categories: Category[] = [] + visitCategoryTree(category, c => categories.push(c)) + return categories +} + +export function categoryUnreadCount(category?: Category): number { + if (!category) return 0 + + return flattenCategoryTree(category) + .flatMap(c => c.feeds) + .map(f => f.unread) + .reduce((total, current) => total + current, 0) +} + +export const calculatePlaceholderSize = ({ width, height, maxWidth }: { width?: number; height?: number; maxWidth: number }) => { + const placeholderWidth = width && Math.min(width, maxWidth) + const placeholderHeight = height && width && width > maxWidth ? height * (maxWidth / width) : height + return { width: placeholderWidth, height: placeholderHeight } +} + +export const scrollToWithCallback = ({ options, onScrollEnded }: { options: ScrollToOptions; onScrollEnded: () => void }) => { + const offset = (options.top ?? 0).toFixed() + + const onScroll = throttle(100, () => { + if (window.scrollY.toFixed() === offset) { + window.removeEventListener("scroll", onScroll) + onScrollEnded() + } + }) + window.addEventListener("scroll", onScroll) + + // scrollTo does not trigger if there's nothing to do, trigger it manually + onScroll() + + window.scrollTo(options) +} + +export const truncate = (str: string, n: number) => (str.length > n ? `${str.slice(0, n - 1)}\u2026` : str) diff --git a/commafeed-client/src/components/ActionButton.tsx b/commafeed-client/src/components/ActionButton.tsx index fdebdc8e..3e86d4ce 100644 --- a/commafeed-client/src/components/ActionButton.tsx +++ b/commafeed-client/src/components/ActionButton.tsx @@ -1,37 +1,37 @@ -import { ActionIcon, Button, type ButtonVariant, Tooltip, useMantineTheme } from "@mantine/core" -import { type ActionIconVariant } from "@mantine/core/lib/components/ActionIcon/ActionIcon" -import { Constants } from "app/constants" -import { useActionButton } from "hooks/useActionButton" -import { forwardRef, type MouseEventHandler, type ReactNode } from "react" - -interface ActionButtonProps { - className?: string - icon?: ReactNode - label: ReactNode - onClick?: MouseEventHandler - variant?: ActionIconVariant & ButtonVariant - hideLabelOnDesktop?: boolean - showLabelOnMobile?: boolean -} - -/** - * Switches between Button with label (desktop) and ActionIcon (mobile) - */ -export const ActionButton = forwardRef((props: ActionButtonProps, ref) => { - const { mobile } = useActionButton() - const theme = useMantineTheme() - const variant = props.variant ?? "subtle" - const iconOnly = (mobile && !props.showLabelOnMobile) || (!mobile && props.hideLabelOnDesktop) - return iconOnly ? ( - - - {props.icon} - - - ) : ( - - ) -}) -ActionButton.displayName = "HeaderButton" +import { ActionIcon, Button, type ButtonVariant, Tooltip, useMantineTheme } from "@mantine/core" +import type { ActionIconVariant } from "@mantine/core/lib/components/ActionIcon/ActionIcon" +import { Constants } from "app/constants" +import { useActionButton } from "hooks/useActionButton" +import { type MouseEventHandler, type ReactNode, forwardRef } from "react" + +interface ActionButtonProps { + className?: string + icon?: ReactNode + label: ReactNode + onClick?: MouseEventHandler + variant?: ActionIconVariant & ButtonVariant + hideLabelOnDesktop?: boolean + showLabelOnMobile?: boolean +} + +/** + * Switches between Button with label (desktop) and ActionIcon (mobile) + */ +export const ActionButton = forwardRef((props: ActionButtonProps, ref) => { + const { mobile } = useActionButton() + const theme = useMantineTheme() + const variant = props.variant ?? "subtle" + const iconOnly = (mobile && !props.showLabelOnMobile) || (!mobile && props.hideLabelOnDesktop) + return iconOnly ? ( + + + {props.icon} + + + ) : ( + + ) +}) +ActionButton.displayName = "HeaderButton" diff --git a/commafeed-client/src/components/Alert.tsx b/commafeed-client/src/components/Alert.tsx index 1725356e..015b59ea 100644 --- a/commafeed-client/src/components/Alert.tsx +++ b/commafeed-client/src/components/Alert.tsx @@ -1,47 +1,47 @@ -import { Trans } from "@lingui/macro" -import { Alert as MantineAlert, Box } from "@mantine/core" -import { Fragment } from "react" -import { TbAlertCircle, TbAlertTriangle, TbCircleCheck } from "react-icons/tb" - -type Level = "error" | "warning" | "success" - -export interface ErrorsAlertProps { - level?: Level - messages: string[] -} - -export function Alert(props: ErrorsAlertProps) { - let title: React.ReactNode - let color: string - let icon: React.ReactNode - - const level = props.level ?? "error" - switch (level) { - case "error": - title = Error - color = "red" - icon = - break - case "warning": - title = Warning - color = "orange" - icon = - break - case "success": - title = Success - color = "green" - icon = - break - } - - return ( - - {props.messages.map((m, i) => ( - - {m} - {i !== props.messages.length - 1 &&
} -
- ))} -
- ) -} +import { Trans } from "@lingui/macro" +import { Box, Alert as MantineAlert } from "@mantine/core" +import { Fragment } from "react" +import { TbAlertCircle, TbAlertTriangle, TbCircleCheck } from "react-icons/tb" + +type Level = "error" | "warning" | "success" + +export interface ErrorsAlertProps { + level?: Level + messages: string[] +} + +export function Alert(props: ErrorsAlertProps) { + let title: React.ReactNode + let color: string + let icon: React.ReactNode + + const level = props.level ?? "error" + switch (level) { + case "error": + title = Error + color = "red" + icon = + break + case "warning": + title = Warning + color = "orange" + icon = + break + case "success": + title = Success + color = "green" + icon = + break + } + + return ( + + {props.messages.map((m, i) => ( + + {m} + {i !== props.messages.length - 1 &&
} +
+ ))} +
+ ) +} diff --git a/commafeed-client/src/components/DisablePullToRefresh.tsx b/commafeed-client/src/components/DisablePullToRefresh.tsx index d8708103..e7b328cc 100644 --- a/commafeed-client/src/components/DisablePullToRefresh.tsx +++ b/commafeed-client/src/components/DisablePullToRefresh.tsx @@ -1,15 +1,15 @@ -import { Helmet } from "react-helmet" - -export const DisablePullToRefresh = () => { - return ( - - - - ) -} +import { Helmet } from "react-helmet" + +export const DisablePullToRefresh = () => { + return ( + + + + ) +} diff --git a/commafeed-client/src/components/ErrorBoundary.tsx b/commafeed-client/src/components/ErrorBoundary.tsx index 2d30e70e..793b2990 100644 --- a/commafeed-client/src/components/ErrorBoundary.tsx +++ b/commafeed-client/src/components/ErrorBoundary.tsx @@ -1,26 +1,26 @@ -import { ErrorPage } from "pages/ErrorPage" -import React, { type ReactNode } from "react" - -interface ErrorBoundaryProps { - children?: ReactNode -} - -interface ErrorBoundaryState { - error?: Error -} - -export class ErrorBoundary extends React.Component { - constructor(props: ErrorBoundaryProps) { - super(props) - this.state = {} - } - - componentDidCatch(error: Error) { - this.setState({ error }) - } - - render() { - if (this.state.error) return - return this.props.children - } -} +import { ErrorPage } from "pages/ErrorPage" +import React, { type ReactNode } from "react" + +interface ErrorBoundaryProps { + children?: ReactNode +} + +interface ErrorBoundaryState { + error?: Error +} + +export class ErrorBoundary extends React.Component { + constructor(props: ErrorBoundaryProps) { + super(props) + this.state = {} + } + + componentDidCatch(error: Error) { + this.setState({ error }) + } + + render() { + if (this.state.error) return + return this.props.children + } +} diff --git a/commafeed-client/src/components/ImageWithPlaceholderWhileLoading.tsx b/commafeed-client/src/components/ImageWithPlaceholderWhileLoading.tsx index bc68fd9c..299772eb 100644 --- a/commafeed-client/src/components/ImageWithPlaceholderWhileLoading.tsx +++ b/commafeed-client/src/components/ImageWithPlaceholderWhileLoading.tsx @@ -1,75 +1,75 @@ -import { Box, Center } from "@mantine/core" -import { useState } from "react" -import { TbPhoto } from "react-icons/tb" -import { tss } from "tss" - -interface ImageWithPlaceholderWhileLoadingProps { - src: string - alt: string - title?: string - width?: number - height?: number | "auto" - placeholderWidth?: number - placeholderHeight?: number - placeholderBackgroundColor?: string - placeholderIconSize?: number -} - -const useStyles = tss - .withParams<{ - placeholderWidth?: number - placeholderHeight?: number - placeholderBackgroundColor?: string - }>() - .create(props => ({ - placeholder: { - width: props.placeholderWidth ?? 400, - height: props.placeholderHeight ?? 600, - maxWidth: "100%", - backgroundColor: - props.placeholderBackgroundColor ?? - (props.colorScheme === "dark" ? props.theme.colors.dark[5] : props.theme.colors.gray[1]), - }, - })) - -export function ImageWithPlaceholderWhileLoading({ - alt, - height, - placeholderBackgroundColor, - placeholderHeight, - placeholderIconSize, - placeholderWidth, - src, - title, - width, -}: ImageWithPlaceholderWhileLoadingProps) { - const { classes } = useStyles({ - placeholderWidth, - placeholderHeight, - placeholderBackgroundColor, - }) - const [loading, setLoading] = useState(true) - - return ( - <> - {loading && ( - -
-
- -
-
-
- )} - {alt} setLoading(false)} - style={{ display: loading ? "none" : "block" }} - /> - - ) -} +import { Box, Center } from "@mantine/core" +import { useState } from "react" +import { TbPhoto } from "react-icons/tb" +import { tss } from "tss" + +interface ImageWithPlaceholderWhileLoadingProps { + src: string + alt: string + title?: string + width?: number + height?: number | "auto" + placeholderWidth?: number + placeholderHeight?: number + placeholderBackgroundColor?: string + placeholderIconSize?: number +} + +const useStyles = tss + .withParams<{ + placeholderWidth?: number + placeholderHeight?: number + placeholderBackgroundColor?: string + }>() + .create(props => ({ + placeholder: { + width: props.placeholderWidth ?? 400, + height: props.placeholderHeight ?? 600, + maxWidth: "100%", + backgroundColor: + props.placeholderBackgroundColor ?? + (props.colorScheme === "dark" ? props.theme.colors.dark[5] : props.theme.colors.gray[1]), + }, + })) + +export function ImageWithPlaceholderWhileLoading({ + alt, + height, + placeholderBackgroundColor, + placeholderHeight, + placeholderIconSize, + placeholderWidth, + src, + title, + width, +}: ImageWithPlaceholderWhileLoadingProps) { + const { classes } = useStyles({ + placeholderWidth, + placeholderHeight, + placeholderBackgroundColor, + }) + const [loading, setLoading] = useState(true) + + return ( + <> + {loading && ( + +
+
+ +
+
+
+ )} + {alt} setLoading(false)} + style={{ display: loading ? "none" : "block" }} + /> + + ) +} diff --git a/commafeed-client/src/components/KeyboardShortcutsHelp.tsx b/commafeed-client/src/components/KeyboardShortcutsHelp.tsx index be04c2cf..77aa50bc 100644 --- a/commafeed-client/src/components/KeyboardShortcutsHelp.tsx +++ b/commafeed-client/src/components/KeyboardShortcutsHelp.tsx @@ -1,224 +1,224 @@ -import { Trans } from "@lingui/macro" -import { Anchor, Box, Kbd, Stack, Table } from "@mantine/core" -import { useOs } from "@mantine/hooks" -import { Constants } from "app/constants" - -export function KeyboardShortcutsHelp() { - const isMacOS = useOs() === "macos" - return ( - - - - - - Refresh - - - R - - - - - Open next entry - - - J - - - - - Open previous entry - - - K - - - - - Set focus on next entry without opening it - - - N - - - - - Set focus on previous entry without opening it - - - P - - - - - Move the page down - - - - Space - - - - - - Move the page up - - - - Shift - - + - - Space - - - - - - Open/close current entry - - - O - , - - Enter - - - - - - Open current entry in a new tab - - - V - - - - - Open current entry in a new tab in the background - - - B - *, - - Middle click - - - - - - Toggle read status of current entry - - - M - , - Swipe header to the left - - - - - Toggle starred status of current entry - - - S - - - - - Mark all entries as read - - - - Shift - - + - A - - - - - Go to the All view - - - G - - A - - - - - Navigate to a subscription by entering its name - - - - {isMacOS ? "Cmd" : "Ctrl"} - - + - K - , - G - - U - - - - - Show entry menu (desktop) - - - - Right click - - - - - - Show native menu (desktop) - - - - Shift - - + - - Right click - - - - - - Show entry menu (mobile) - - - - Long press - - - - - - Toggle sidebar - - - F - - - - - Show keyboard shortcut help - - - ? - - - -
- - * - - Browser extension required for Chrome - - -
- ) -} +import { Trans } from "@lingui/macro" +import { Anchor, Box, Kbd, Stack, Table } from "@mantine/core" +import { useOs } from "@mantine/hooks" +import { Constants } from "app/constants" + +export function KeyboardShortcutsHelp() { + const isMacOS = useOs() === "macos" + return ( + + + + + + Refresh + + + R + + + + + Open next entry + + + J + + + + + Open previous entry + + + K + + + + + Set focus on next entry without opening it + + + N + + + + + Set focus on previous entry without opening it + + + P + + + + + Move the page down + + + + Space + + + + + + Move the page up + + + + Shift + + + + + Space + + + + + + Open/close current entry + + + O + , + + Enter + + + + + + Open current entry in a new tab + + + V + + + + + Open current entry in a new tab in the background + + + B + *, + + Middle click + + + + + + Toggle read status of current entry + + + M + , + Swipe header to the left + + + + + Toggle starred status of current entry + + + S + + + + + Mark all entries as read + + + + Shift + + + + A + + + + + Go to the All view + + + G + + A + + + + + Navigate to a subscription by entering its name + + + + {isMacOS ? "Cmd" : "Ctrl"} + + + + K + , + G + + U + + + + + Show entry menu (desktop) + + + + Right click + + + + + + Show native menu (desktop) + + + + Shift + + + + + Right click + + + + + + Show entry menu (mobile) + + + + Long press + + + + + + Toggle sidebar + + + F + + + + + Show keyboard shortcut help + + + ? + + + +
+ + * + + Browser extension required for Chrome + + +
+ ) +} diff --git a/commafeed-client/src/components/Loader.tsx b/commafeed-client/src/components/Loader.tsx index 6bc6930d..b6eeee4e 100644 --- a/commafeed-client/src/components/Loader.tsx +++ b/commafeed-client/src/components/Loader.tsx @@ -1,9 +1,9 @@ -import { Center, Loader as MantineLoader } from "@mantine/core" - -export function Loader() { - return ( -
- -
- ) -} +import { Center, Loader as MantineLoader } from "@mantine/core" + +export function Loader() { + return ( +
+ +
+ ) +} diff --git a/commafeed-client/src/components/Logo.tsx b/commafeed-client/src/components/Logo.tsx index a12016fd..c303b589 100644 --- a/commafeed-client/src/components/Logo.tsx +++ b/commafeed-client/src/components/Logo.tsx @@ -1,10 +1,10 @@ -import { Image } from "@mantine/core" -import logo from "assets/logo.svg" - -export interface LogoProps { - size: number -} - -export function Logo(props: LogoProps) { - return -} +import { Image } from "@mantine/core" +import logo from "assets/logo.svg" + +export interface LogoProps { + size: number +} + +export function Logo(props: LogoProps) { + return +} diff --git a/commafeed-client/src/components/RelativeDate.tsx b/commafeed-client/src/components/RelativeDate.tsx index 8e3f7ed7..769d8e74 100644 --- a/commafeed-client/src/components/RelativeDate.tsx +++ b/commafeed-client/src/components/RelativeDate.tsx @@ -1,21 +1,21 @@ -import { Trans } from "@lingui/macro" -import { Tooltip } from "@mantine/core" -import { Constants } from "app/constants" -import dayjs from "dayjs" -import { useEffect, useState } from "react" - -export function RelativeDate(props: { date: Date | number | undefined }) { - const [now, setNow] = useState(new Date()) - useEffect(() => { - const interval = setInterval(() => setNow(new Date()), 60 * 1000) - return () => clearInterval(interval) - }, []) - - if (!props.date) return N/A - const date = dayjs(props.date) - return ( - - {date.from(dayjs(now))} - - ) -} +import { Trans } from "@lingui/macro" +import { Tooltip } from "@mantine/core" +import { Constants } from "app/constants" +import dayjs from "dayjs" +import { useEffect, useState } from "react" + +export function RelativeDate(props: { date: Date | number | undefined }) { + const [now, setNow] = useState(new Date()) + useEffect(() => { + const interval = setInterval(() => setNow(new Date()), 60 * 1000) + return () => clearInterval(interval) + }, []) + + if (!props.date) return N/A + const date = dayjs(props.date) + return ( + + {date.from(dayjs(now))} + + ) +} diff --git a/commafeed-client/src/components/admin/UserEdit.tsx b/commafeed-client/src/components/admin/UserEdit.tsx index 63b15f25..f10014f2 100644 --- a/commafeed-client/src/components/admin/UserEdit.tsx +++ b/commafeed-client/src/components/admin/UserEdit.tsx @@ -1,54 +1,54 @@ -import { Trans } from "@lingui/macro" -import { Box, Button, Checkbox, Group, PasswordInput, Stack, TextInput } from "@mantine/core" -import { useForm } from "@mantine/form" -import { client, errorToStrings } from "app/client" -import { type AdminSaveUserRequest, type UserModel } from "app/types" -import { Alert } from "components/Alert" -import { useAsyncCallback } from "react-async-hook" -import { TbDeviceFloppy } from "react-icons/tb" - -interface UserEditProps { - user?: UserModel - onCancel: () => void - onSave: () => void -} - -export function UserEdit(props: UserEditProps) { - const form = useForm({ - initialValues: props.user ?? { - name: "", - enabled: true, - admin: false, - }, - }) - const saveUser = useAsyncCallback(client.admin.saveUser, { onSuccess: props.onSave }) - - return ( - <> - {saveUser.error && ( - - - - )} - -
- - Name} {...form.getInputProps("name")} required /> - Password} {...form.getInputProps("password")} required={!props.user} /> - E-mail} {...form.getInputProps("email")} /> - Admin} {...form.getInputProps("admin", { type: "checkbox" })} /> - Enabled} {...form.getInputProps("enabled", { type: "checkbox" })} /> - - - - - - -
- - ) -} +import { Trans } from "@lingui/macro" +import { Box, Button, Checkbox, Group, PasswordInput, Stack, TextInput } from "@mantine/core" +import { useForm } from "@mantine/form" +import { client, errorToStrings } from "app/client" +import type { AdminSaveUserRequest, UserModel } from "app/types" +import { Alert } from "components/Alert" +import { useAsyncCallback } from "react-async-hook" +import { TbDeviceFloppy } from "react-icons/tb" + +interface UserEditProps { + user?: UserModel + onCancel: () => void + onSave: () => void +} + +export function UserEdit(props: UserEditProps) { + const form = useForm({ + initialValues: props.user ?? { + name: "", + enabled: true, + admin: false, + }, + }) + const saveUser = useAsyncCallback(client.admin.saveUser, { onSuccess: props.onSave }) + + return ( + <> + {saveUser.error && ( + + + + )} + +
+ + Name} {...form.getInputProps("name")} required /> + Password} {...form.getInputProps("password")} required={!props.user} /> + E-mail} {...form.getInputProps("email")} /> + Admin} {...form.getInputProps("admin", { type: "checkbox" })} /> + Enabled} {...form.getInputProps("enabled", { type: "checkbox" })} /> + + + + + + +
+ + ) +} diff --git a/commafeed-client/src/components/code/CodeEditor.tsx b/commafeed-client/src/components/code/CodeEditor.tsx index 2345cca0..b4d1058b 100644 --- a/commafeed-client/src/components/code/CodeEditor.tsx +++ b/commafeed-client/src/components/code/CodeEditor.tsx @@ -1,36 +1,36 @@ -import { Input, Textarea } from "@mantine/core" -import RichCodeEditor from "components/code/RichCodeEditor" -import { useMobile } from "hooks/useMobile" -import { type ReactNode } from "react" - -interface CodeEditorProps { - description?: ReactNode - language: "css" | "javascript" - value?: string - onChange: (value: string | undefined) => void -} - -export function CodeEditor(props: CodeEditorProps) { - const mobile = useMobile() - - return mobile ? ( - // monaco mobile support is poor, fallback to textarea -