diff --git a/package-lock.json b/package-lock.json index ec3a875..9b339e2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,13 +16,14 @@ "clsx": "^2.1.1", "framer-motion": "^11.9.0", "highlight.js": "^11.11.1", - "monaco-editor": "^0.54.0", + "monaco-editor": "^0.53.0", "postcss": "^8.4.47", "react": "^18.3.1", "react-dom": "^18.3.1", "react-markdown": "^10.1.0", "react-redux": "^9.2.0", "react-router-dom": "^7.9.4", + "react-toastify": "^11.0.5", "rehype-highlight": "^7.0.2", "rehype-raw": "^7.0.0", "rehype-sanitize": "^6.0.0", @@ -43,7 +44,7 @@ "globals": "^15.9.0", "typescript": "^5.5.3", "typescript-eslint": "^8.0.1", - "vite": "^5.4.1" + "vite": "^7.2.2" } }, "node_modules/@alloc/quick-lru": { @@ -58,38 +59,25 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@babel/code-frame": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", - "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/highlight": "^7.24.7", - "picocolors": "^1.0.0" + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/compat-data": { - "version": "7.25.4", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.4.tgz", - "integrity": "sha512-+LGRog6RAsCJrrrg/IO6LGmpphNe5DiK30dGjCoxxeGv49B10/3XYGxPsAwrDlMFcFEvdAUavDT8r9k/hSyQqQ==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", "dev": true, "license": "MIT", "engines": { @@ -97,22 +85,22 @@ } }, "node_modules/@babel/core": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz", - "integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.25.0", - "@babel/helper-compilation-targets": "^7.25.2", - "@babel/helper-module-transforms": "^7.25.2", - "@babel/helpers": "^7.25.0", - "@babel/parser": "^7.25.0", - "@babel/template": "^7.25.0", - "@babel/traverse": "^7.25.2", - "@babel/types": "^7.25.2", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -128,31 +116,32 @@ } }, "node_modules/@babel/generator": { - "version": "7.25.6", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.6.tgz", - "integrity": "sha512-VPC82gr1seXOpkjAAKoLhP50vx4vGNlF4msF64dSFq1P8RfB+QAuJWGHPXXPc8QyfVWwwB/TNNU4+ayZmHNbZw==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.25.6", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^2.5.1" + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz", - "integrity": "sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.25.2", - "@babel/helper-validator-option": "^7.24.8", - "browserslist": "^4.23.1", + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" }, @@ -160,31 +149,40 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-module-imports": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", - "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.2.tgz", - "integrity": "sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.24.7", - "@babel/helper-simple-access": "^7.24.7", - "@babel/helper-validator-identifier": "^7.24.7", - "@babel/traverse": "^7.25.2" + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" }, "engines": { "node": ">=6.9.0" @@ -194,33 +192,19 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz", - "integrity": "sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/helper-simple-access": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", - "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-string-parser": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", - "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "dev": true, "license": "MIT", "engines": { @@ -228,9 +212,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", - "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "dev": true, "license": "MIT", "engines": { @@ -238,9 +222,9 @@ } }, "node_modules/@babel/helper-validator-option": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz", - "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", "dev": true, "license": "MIT", "engines": { @@ -248,43 +232,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.25.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.6.tgz", - "integrity": "sha512-Xg0tn4HcfTijTwfDwYlvVCl43V6h4KyVVX2aEm4qdO/PC6L2YvzLHFdmxhoeSA3eslcE6+ZVXHgWwopXYLNq4Q==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.25.0", - "@babel/types": "^7.25.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", - "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.24.7", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.25.6", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.6.tgz", - "integrity": "sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.25.6" + "@babel/types": "^7.28.5" }, "bin": { "parser": "bin/babel-parser.js" @@ -294,13 +262,13 @@ } }, "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.24.7.tgz", - "integrity": "sha512-fOPQYbGSgH0HUp4UJO4sMBFjY6DuWq+2i8rixyUMb3CdGixs/gccURvYOAhajBdKDoGajFr3mUq5rH3phtkGzw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -310,13 +278,13 @@ } }, "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.24.7.tgz", - "integrity": "sha512-J2z+MWzZHVOemyLweMqngXrgGC42jQ//R0KdxqkIz/OrbVIIlhFI3WigZ5fO+nwFvBlncr4MGapd8vTyc7RPNQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -326,80 +294,66 @@ } }, "node_modules/@babel/runtime": { - "version": "7.25.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.6.tgz", - "integrity": "sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", "license": "MIT", - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/template": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.0.tgz", - "integrity": "sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.24.7", - "@babel/parser": "^7.25.0", - "@babel/types": "^7.25.0" + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.25.6", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.6.tgz", - "integrity": "sha512-9Vrcx5ZW6UwK5tvqsj0nGpp/XzqthkT0dqIc9g1AdtygFToNtTF67XzYS//dm+SAK9cp3B9R4ZO/46p63SCjlQ==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.25.6", - "@babel/parser": "^7.25.6", - "@babel/template": "^7.25.0", - "@babel/types": "^7.25.6", - "debug": "^4.3.1", - "globals": "^11.1.0" + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/traverse/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/@babel/types": { - "version": "7.25.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.6.tgz", - "integrity": "sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.24.8", - "@babel/helper-validator-identifier": "^7.24.7", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", "cpu": [ "ppc64" ], @@ -410,13 +364,13 @@ "aix" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", "cpu": [ "arm" ], @@ -427,13 +381,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", "cpu": [ "arm64" ], @@ -444,13 +398,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", "cpu": [ "x64" ], @@ -461,13 +415,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", "cpu": [ "arm64" ], @@ -478,13 +432,13 @@ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", "cpu": [ "x64" ], @@ -495,13 +449,13 @@ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", "cpu": [ "arm64" ], @@ -512,13 +466,13 @@ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", "cpu": [ "x64" ], @@ -529,13 +483,13 @@ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", "cpu": [ "arm" ], @@ -546,13 +500,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", "cpu": [ "arm64" ], @@ -563,13 +517,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", "cpu": [ "ia32" ], @@ -580,13 +534,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", "cpu": [ "loong64" ], @@ -597,13 +551,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", "cpu": [ "mips64el" ], @@ -614,13 +568,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", "cpu": [ "ppc64" ], @@ -631,13 +585,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", "cpu": [ "riscv64" ], @@ -648,13 +602,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", "cpu": [ "s390x" ], @@ -665,13 +619,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", "cpu": [ "x64" ], @@ -682,13 +636,30 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", "cpu": [ "x64" ], @@ -699,13 +670,30 @@ "netbsd" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", "cpu": [ "x64" ], @@ -716,13 +704,30 @@ "openbsd" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", "cpu": [ "x64" ], @@ -733,13 +738,13 @@ "sunos" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", "cpu": [ "arm64" ], @@ -750,13 +755,13 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", "cpu": [ "ia32" ], @@ -767,13 +772,13 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", "cpu": [ "x64" ], @@ -784,21 +789,24 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "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==", + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", "dev": true, "license": "MIT", "dependencies": { - "eslint-visitor-keys": "^3.3.0" + "eslint-visitor-keys": "^3.4.3" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, + "funding": { + "url": "https://opencollective.com/eslint" + }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } @@ -817,9 +825,9 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.11.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.1.tgz", - "integrity": "sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==", + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true, "license": "MIT", "engines": { @@ -827,13 +835,13 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.18.0.tgz", - "integrity": "sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==", + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^2.1.4", + "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.2" }, @@ -841,10 +849,36 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@eslint/eslintrc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", - "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", "dev": true, "license": "MIT", "dependencies": { @@ -879,19 +913,22 @@ } }, "node_modules/@eslint/js": { - "version": "9.10.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.10.0.tgz", - "integrity": "sha512-fuXtbiP5GWIn8Fz+LWoOMVf/Jxm+aajZYkhi6CuEm4SxymFM+eUWzbO9qXT+L0iCkL5+KGYMCSGxo686H19S1g==", + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz", + "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", "dev": true, "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" } }, "node_modules/@eslint/object-schema": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", - "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -899,18 +936,43 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.1.0.tgz", - "integrity": "sha512-autAXT203ixhqei9xt+qkYOvY8l6LAFIdT2UXc/RPNeUVfqRF1BV94GTJyVPFKT8nFM6MyVJhjLj9E8JWvf5zQ==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", "dev": true, "license": "Apache-2.0", "dependencies": { + "@eslint/core": "^0.17.0", "levn": "^0.4.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -926,9 +988,9 @@ } }, "node_modules/@humanwhocodes/retry": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz", - "integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==", + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -984,17 +1046,24 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "license": "MIT", "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" } }, "node_modules/@jridgewell/resolve-uri": { @@ -1006,15 +1075,6 @@ "node": ">=6.0.0" } }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", @@ -1022,9 +1082,9 @@ "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -1125,10 +1185,17 @@ } } }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.5.tgz", - "integrity": "sha512-SU5cvamg0Eyu/F+kLeMXS7GoahL+OoizlclVFX3l5Ql6yNlywJJ0OuqTzUx0v+aHhPHEB/56CT06GQrRrGNYww==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.2.tgz", + "integrity": "sha512-yDPzwsgiFO26RJA4nZo8I+xqzh7sJTZIWQOxn+/XOdPE31lAvLIYCKqjV+lNH/vxE2L2iH3plKxDCRK6i+CwhA==", "cpu": [ "arm" ], @@ -1140,9 +1207,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.22.5.tgz", - "integrity": "sha512-S4pit5BP6E5R5C8S6tgU/drvgjtYW76FBuG6+ibG3tMvlD1h9LHVF9KmlmaUBQ8Obou7hEyS+0w+IR/VtxwNMQ==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.2.tgz", + "integrity": "sha512-k8FontTxIE7b0/OGKeSN5B6j25EuppBcWM33Z19JoVT7UTXFSo3D9CdU39wGTeb29NO3XxpMNauh09B+Ibw+9g==", "cpu": [ "arm64" ], @@ -1154,9 +1221,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.5.tgz", - "integrity": "sha512-250ZGg4ipTL0TGvLlfACkIxS9+KLtIbn7BCZjsZj88zSg2Lvu3Xdw6dhAhfe/FjjXPVNCtcSp+WZjVsD3a/Zlw==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.2.tgz", + "integrity": "sha512-A6s4gJpomNBtJ2yioj8bflM2oogDwzUiMl2yNJ2v9E7++sHrSrsQ29fOfn5DM/iCzpWcebNYEdXpaK4tr2RhfQ==", "cpu": [ "arm64" ], @@ -1168,9 +1235,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.22.5.tgz", - "integrity": "sha512-D8brJEFg5D+QxFcW6jYANu+Rr9SlKtTenmsX5hOSzNYVrK5oLAEMTUgKWYJP+wdKyCdeSwnapLsn+OVRFycuQg==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.2.tgz", + "integrity": "sha512-e6XqVmXlHrBlG56obu9gDRPW3O3hLxpwHpLsBJvuI8qqnsrtSZ9ERoWUXtPOkY8c78WghyPHZdmPhHLWNdAGEw==", "cpu": [ "x64" ], @@ -1181,10 +1248,38 @@ "darwin" ] }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.2.tgz", + "integrity": "sha512-v0E9lJW8VsrwPux5Qe5CwmH/CF/2mQs6xU1MF3nmUxmZUCHazCjLgYvToOk+YuuUqLQBio1qkkREhxhc656ViA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.2.tgz", + "integrity": "sha512-ClAmAPx3ZCHtp6ysl4XEhWU69GUB1D+s7G9YjHGhIGCSrsg00nEGRRZHmINYxkdoJehde8VIsDC5t9C0gb6yqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.22.5.tgz", - "integrity": "sha512-PNqXYmdNFyWNg0ma5LdY8wP+eQfdvyaBAojAXgO7/gs0Q/6TQJVXAXe8gwW9URjbS0YAammur0fynYGiWsKlXw==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.2.tgz", + "integrity": "sha512-EPlb95nUsz6Dd9Qy13fI5kUPXNSljaG9FiJ4YUGU1O/Q77i5DYFW5KR8g1OzTcdZUqQQ1KdDqsTohdFVwCwjqg==", "cpu": [ "arm" ], @@ -1196,9 +1291,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.22.5.tgz", - "integrity": "sha512-kSSCZOKz3HqlrEuwKd9TYv7vxPYD77vHSUvM2y0YaTGnFc8AdI5TTQRrM1yIp3tXCKrSL9A7JLoILjtad5t8pQ==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.2.tgz", + "integrity": "sha512-BOmnVW+khAUX+YZvNfa0tGTEMVVEerOxN0pDk2E6N6DsEIa2Ctj48FOMfNDdrwinocKaC7YXUZ1pHlKpnkja/Q==", "cpu": [ "arm" ], @@ -1210,9 +1305,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.22.5.tgz", - "integrity": "sha512-oTXQeJHRbOnwRnRffb6bmqmUugz0glXaPyspp4gbQOPVApdpRrY/j7KP3lr7M8kTfQTyrBUzFjj5EuHAhqH4/w==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.2.tgz", + "integrity": "sha512-Xt2byDZ+6OVNuREgBXr4+CZDJtrVso5woFtpKdGPhpTPHcNG7D8YXeQzpNbFRxzTVqJf7kvPMCub/pcGUWgBjA==", "cpu": [ "arm64" ], @@ -1224,9 +1319,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.22.5.tgz", - "integrity": "sha512-qnOTIIs6tIGFKCHdhYitgC2XQ2X25InIbZFor5wh+mALH84qnFHvc+vmWUpyX97B0hNvwNUL4B+MB8vJvH65Fw==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.2.tgz", + "integrity": "sha512-+LdZSldy/I9N8+klim/Y1HsKbJ3BbInHav5qE9Iy77dtHC/pibw1SR/fXlWyAk0ThnpRKoODwnAuSjqxFRDHUQ==", "cpu": [ "arm64" ], @@ -1237,10 +1332,24 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.22.5.tgz", - "integrity": "sha512-TMYu+DUdNlgBXING13rHSfUc3Ky5nLPbWs4bFnT+R6Vu3OvXkTkixvvBKk8uO4MT5Ab6lC3U7x8S8El2q5o56w==", + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.2.tgz", + "integrity": "sha512-8ms8sjmyc1jWJS6WdNSA23rEfdjWB30LH8Wqj0Cqvv7qSHnvw6kgMMXRdop6hkmGPlyYBdRPkjJnj3KCUHV/uQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.2.tgz", + "integrity": "sha512-3HRQLUQbpBDMmzoxPJYd3W6vrVHOo2cVW8RUo87Xz0JPJcBLBr5kZ1pGcQAhdZgX9VV7NbGNipah1omKKe23/g==", "cpu": [ "ppc64" ], @@ -1252,9 +1361,23 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.22.5.tgz", - "integrity": "sha512-PTQq1Kz22ZRvuhr3uURH+U/Q/a0pbxJoICGSprNLAoBEkyD3Sh9qP5I0Asn0y0wejXQBbsVMRZRxlbGFD9OK4A==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.2.tgz", + "integrity": "sha512-fMjKi+ojnmIvhk34gZP94vjogXNNUKMEYs+EDaB/5TG/wUkoeua7p7VCHnE6T2Tx+iaghAqQX8teQzcvrYpaQA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.2.tgz", + "integrity": "sha512-XuGFGU+VwUUV5kLvoAdi0Wz5Xbh2SrjIxCtZj6Wq8MDp4bflb/+ThZsVxokM7n0pcbkEr2h5/pzqzDYI7cCgLQ==", "cpu": [ "riscv64" ], @@ -1266,9 +1389,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.22.5.tgz", - "integrity": "sha512-bR5nCojtpuMss6TDEmf/jnBnzlo+6n1UhgwqUvRoe4VIotC7FG1IKkyJbwsT7JDsF2jxR+NTnuOwiGv0hLyDoQ==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.2.tgz", + "integrity": "sha512-w6yjZF0P+NGzWR3AXWX9zc0DNEGdtvykB03uhonSHMRa+oWA6novflo2WaJr6JZakG2ucsyb+rvhrKac6NIy+w==", "cpu": [ "s390x" ], @@ -1280,9 +1403,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.5.tgz", - "integrity": "sha512-N0jPPhHjGShcB9/XXZQWuWBKZQnC1F36Ce3sDqWpujsGjDz/CQtOL9LgTrJ+rJC8MJeesMWrMWVLKKNR/tMOCA==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.2.tgz", + "integrity": "sha512-yo8d6tdfdeBArzC7T/PnHd7OypfI9cbuZzPnzLJIyKYFhAQ8SvlkKtKBMbXDxe1h03Rcr7u++nFS7tqXz87Gtw==", "cpu": [ "x64" ], @@ -1294,9 +1417,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.22.5.tgz", - "integrity": "sha512-uBa2e28ohzNNwjr6Uxm4XyaA1M/8aTgfF2T7UIlElLaeXkgpmIJ2EitVNQxjO9xLLLy60YqAgKn/AqSpCUkE9g==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.2.tgz", + "integrity": "sha512-ah59c1YkCxKExPP8O9PwOvs+XRLKwh/mV+3YdKqQ5AMQ0r4M4ZDuOrpWkUaqO7fzAHdINzV9tEVu8vNw48z0lA==", "cpu": [ "x64" ], @@ -1307,10 +1430,24 @@ "linux" ] }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.2.tgz", + "integrity": "sha512-4VEd19Wmhr+Zy7hbUsFZ6YXEiP48hE//KPLCSVNY5RMGX2/7HZ+QkN55a3atM1C/BZCGIgqN+xrVgtdak2S9+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.22.5.tgz", - "integrity": "sha512-RXT8S1HP8AFN/Kr3tg4fuYrNxZ/pZf1HemC5Tsddc6HzgGnJm0+Lh5rAHJkDuW3StI0ynNXukidROMXYl6ew8w==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.2.tgz", + "integrity": "sha512-IlbHFYc/pQCgew/d5fslcy1KEaYVCJ44G8pajugd8VoOEI8ODhtb/j8XMhLpwHCMB3yk2J07ctup10gpw2nyMA==", "cpu": [ "arm64" ], @@ -1322,9 +1459,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.22.5.tgz", - "integrity": "sha512-ElTYOh50InL8kzyUD6XsnPit7jYCKrphmddKAe1/Ytt74apOxDq5YEcbsiKs0fR3vff3jEneMM+3I7jbqaMyBg==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.2.tgz", + "integrity": "sha512-lNlPEGgdUfSzdCWU176ku/dQRnA7W+Gp8d+cWv73jYrb8uT7HTVVxq62DUYxjbaByuf1Yk0RIIAbDzp+CnOTFg==", "cpu": [ "ia32" ], @@ -1335,10 +1472,24 @@ "win32" ] }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.2.tgz", + "integrity": "sha512-S6YojNVrHybQis2lYov1sd+uj7K0Q05NxHcGktuMMdIQ2VixGwAfbJ23NnlvvVV1bdpR2m5MsNBViHJKcA4ADw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.22.5.tgz", - "integrity": "sha512-+lvL/4mQxSV8MukpkKyyvfwhH266COcWlXE/1qxwN08ajovta3459zrjLghYMgDerlzNwLAcFpvU+WWE5y6nAQ==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.2.tgz", + "integrity": "sha512-k+/Rkcyx//P6fetPoLMb8pBeqJBNGx81uuf7iljX9++yNBVRDQgD04L+SVXmXmh5ZP4/WOp4mWF0kmi06PW2tA==", "cpu": [ "x64" ], @@ -1441,9 +1592,9 @@ } }, "node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "license": "MIT" }, "node_modules/@types/estree-jsx": { @@ -1474,6 +1625,13 @@ "hoist-non-react-statics": "^3.3.0" } }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/mdast": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", @@ -1536,6 +1694,12 @@ "@babel/runtime": "^7.9.2" } }, + "node_modules/@types/trusted-types": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-1.0.6.tgz", + "integrity": "sha512-230RC8sFeHoT6sSUlRO6a8cAnclO06eeiq1QDfiv2FGCLWFvvERWgwIQD4FWqD9A69BN7Lzee4OXwoMVnnsWDw==", + "license": "MIT" + }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -1698,9 +1862,9 @@ } }, "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==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1797,29 +1961,30 @@ "license": "ISC" }, "node_modules/@vitejs/plugin-react": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.1.tgz", - "integrity": "sha512-m/V2syj5CuVnaxcUJOQRel/Wr31FFXRFlnOoq1TVtkCxsY5veGMTEmpWHndrhB2U8ScHtCQB1e+4hWYExQc6Lg==", + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/core": "^7.24.5", - "@babel/plugin-transform-react-jsx-self": "^7.24.5", - "@babel/plugin-transform-react-jsx-source": "^7.24.1", + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", - "react-refresh": "^0.14.2" + "react-refresh": "^0.17.0" }, "engines": { "node": "^14.18.0 || >=16.0.0" }, "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0" + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "node_modules/acorn": { - "version": "8.12.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", - "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", "bin": { @@ -1865,19 +2030,6 @@ "node": ">=8" } }, - "node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/any-promise": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", @@ -1981,6 +2133,16 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.28", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.28.tgz", + "integrity": "sha512-gYjt7OIqdM0PcttNYP2aVrr2G0bMALkBaoehD4BuRGjAOtipg0b6wHg1yNL+s5zSnLZZrGHOw4IrND8CD+3oIQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -1994,9 +2156,9 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -2017,9 +2179,9 @@ } }, "node_modules/browserslist": { - "version": "4.23.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz", - "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.0.tgz", + "integrity": "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==", "dev": true, "funding": [ { @@ -2037,10 +2199,11 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001646", - "electron-to-chromium": "^1.5.4", - "node-releases": "^2.0.18", - "update-browserslist-db": "^1.1.0" + "baseline-browser-mapping": "^2.8.25", + "caniuse-lite": "^1.0.30001754", + "electron-to-chromium": "^1.5.249", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.1.4" }, "bin": { "browserslist": "cli.js" @@ -2082,9 +2245,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001751", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001751.tgz", - "integrity": "sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==", + "version": "1.0.30001754", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001754.tgz", + "integrity": "sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg==", "dev": true, "funding": [ { @@ -2112,21 +2275,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/character-entities": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", @@ -2212,23 +2360,6 @@ "node": ">=6" } }, - "node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true, - "license": "MIT" - }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -2284,9 +2415,9 @@ } }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -2395,12 +2526,6 @@ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", "license": "MIT" }, - "node_modules/dompurify": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.7.tgz", - "integrity": "sha512-VaTstWtsneJY8xzy7DekmYWEOZcmzIe3Qb3zPd4STve1OBTa+e+WmS1ITQec1fZYXI3HCsOZZiSMpG6oxoWMWQ==", - "license": "(MPL-2.0 OR Apache-2.0)" - }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -2422,9 +2547,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.26", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.26.tgz", - "integrity": "sha512-Z+OMe9M/V6Ep9n/52+b7lkvYEps26z4Yz3vjWL1V61W0q+VLF1pOHhMY17sa4roz4AWmULSI8E6SAojZA5L0YQ==", + "version": "1.5.253", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.253.tgz", + "integrity": "sha512-O0tpQ/35rrgdiGQ0/OFWhy1itmd9A6TY9uQzlqj3hKSu/aYpe7UIn5d7CU2N9myH6biZiWF3VMZVuup8pw5U9w==", "dev": true, "license": "ISC" }, @@ -2492,9 +2617,9 @@ } }, "node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -2502,32 +2627,35 @@ "esbuild": "bin/esbuild" }, "engines": { - "node": ">=12" + "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" } }, "node_modules/escalade": { @@ -2540,40 +2668,33 @@ "node": ">=6" } }, - "node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/eslint": { - "version": "9.10.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.10.0.tgz", - "integrity": "sha512-Y4D0IgtBZfOcOUAIQTSXBKoNGfY0REGqHJG6+Q81vNippW5YlKjHFj4soMxamKK1NXHUWuBZTLdU3Km+L/pcHw==", + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", + "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.11.0", - "@eslint/config-array": "^0.18.0", - "@eslint/eslintrc": "^3.1.0", - "@eslint/js": "9.10.0", - "@eslint/plugin-kit": "^0.1.0", + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.1", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.3.0", - "@nodelib/fs.walk": "^1.2.8", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", "ajv": "^6.12.4", "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", + "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.0.2", - "eslint-visitor-keys": "^4.0.0", - "espree": "^10.1.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -2583,14 +2704,11 @@ "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", "json-stable-stringify-without-jsonify": "^1.0.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" + "optionator": "^0.9.3" }, "bin": { "eslint": "bin/eslint.js" @@ -2634,9 +2752,9 @@ } }, "node_modules/eslint-scope": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.2.tgz", - "integrity": "sha512-6E4xmrTw5wtxnLA5wYL3WDfhZ/1bUBGOXV0zQvVRDOtrR8D0p6W7fs3JweNYhwRYeGvd/1CKX2se0/2s7Q/nJA==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -2651,9 +2769,9 @@ } }, "node_modules/eslint-visitor-keys": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", - "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -2753,15 +2871,15 @@ } }, "node_modules/espree": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz", - "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.12.0", + "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.0.0" + "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3147,9 +3265,9 @@ } }, "node_modules/glob/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==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -3202,16 +3320,6 @@ "dev": true, "license": "MIT" }, - "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -3498,9 +3606,9 @@ } }, "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3640,16 +3748,6 @@ "node": ">=0.12.0" } }, - "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, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/is-plain-obj": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", @@ -3699,9 +3797,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { @@ -3712,16 +3810,16 @@ } }, "node_modules/jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "dev": true, "license": "MIT", "bin": { "jsesc": "bin/jsesc" }, "engines": { - "node": ">=4" + "node": ">=6" } }, "node_modules/json-buffer": { @@ -3877,18 +3975,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/marked": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz", - "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==", - "license": "MIT", - "bin": { - "marked": "bin/marked.js" - }, - "engines": { - "node": ">= 18" - } - }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -4809,13 +4895,12 @@ } }, "node_modules/monaco-editor": { - "version": "0.54.0", - "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.54.0.tgz", - "integrity": "sha512-hx45SEUoLatgWxHKCmlLJH81xBo0uXP4sRkESUpmDQevfi+e7K1VuiSprK6UpQ8u4zOcKNiH0pMvHvlMWA/4cw==", + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.53.0.tgz", + "integrity": "sha512-0WNThgC6CMWNXXBxTbaYYcunj08iB5rnx4/G56UOPeL9UVIUGGHA1GR0EWIh9Ebabj7NpCRawQ5b0hfN1jQmYQ==", "license": "MIT", "dependencies": { - "dompurify": "3.1.7", - "marked": "14.0.0" + "@types/trusted-types": "^1.0.6" } }, "node_modules/ms": { @@ -4836,9 +4921,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "funding": [ { "type": "github", @@ -4861,9 +4946,9 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", - "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", "dev": true, "license": "MIT" }, @@ -5058,9 +5143,9 @@ "license": "ISC" }, "node_modules/picocolors": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", - "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "license": "ISC" }, "node_modules/picomatch": { @@ -5094,9 +5179,9 @@ } }, "node_modules/postcss": { - "version": "8.4.47", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", - "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "funding": [ { "type": "opencollective", @@ -5113,8 +5198,8 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.7", - "picocolors": "^1.1.0", + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, "engines": { @@ -5386,9 +5471,9 @@ } }, "node_modules/react-refresh": { - "version": "0.14.2", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", - "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", "dev": true, "license": "MIT", "engines": { @@ -5433,6 +5518,19 @@ "react-dom": ">=18" } }, + "node_modules/react-toastify": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-11.0.5.tgz", + "integrity": "sha512-EpqHBGvnSTtHYhCPLxML05NLY2ZX0JURbAdNYa6BUkk+amz4wbKBQvoKQAB0ardvSarUBuY4Q4s1sluAzZwkmA==", + "license": "MIT", + "dependencies": { + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": "^18 || ^19", + "react-dom": "^18 || ^19" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -5469,12 +5567,6 @@ "redux": "^5.0.0" } }, - "node_modules/regenerator-runtime": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", - "license": "MIT" - }, "node_modules/rehype-highlight": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/rehype-highlight/-/rehype-highlight-7.0.2.tgz", @@ -5631,13 +5723,13 @@ } }, "node_modules/rollup": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.5.tgz", - "integrity": "sha512-WoinX7GeQOFMGznEcWA1WrTQCd/tpEbMkc3nuMs9BT0CPjMdSjPMTVClwWd4pgSQwJdP65SK9mTCNvItlr5o7w==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.2.tgz", + "integrity": "sha512-MHngMYwGJVi6Fmnk6ISmnk7JAHRNF0UkuucA0CUW3N3a4KnONPEZz+vUanQP/ZC/iY1Qkf3bwPWzyY84wEks1g==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.6" + "@types/estree": "1.0.8" }, "bin": { "rollup": "dist/bin/rollup" @@ -5647,22 +5739,28 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.22.5", - "@rollup/rollup-android-arm64": "4.22.5", - "@rollup/rollup-darwin-arm64": "4.22.5", - "@rollup/rollup-darwin-x64": "4.22.5", - "@rollup/rollup-linux-arm-gnueabihf": "4.22.5", - "@rollup/rollup-linux-arm-musleabihf": "4.22.5", - "@rollup/rollup-linux-arm64-gnu": "4.22.5", - "@rollup/rollup-linux-arm64-musl": "4.22.5", - "@rollup/rollup-linux-powerpc64le-gnu": "4.22.5", - "@rollup/rollup-linux-riscv64-gnu": "4.22.5", - "@rollup/rollup-linux-s390x-gnu": "4.22.5", - "@rollup/rollup-linux-x64-gnu": "4.22.5", - "@rollup/rollup-linux-x64-musl": "4.22.5", - "@rollup/rollup-win32-arm64-msvc": "4.22.5", - "@rollup/rollup-win32-ia32-msvc": "4.22.5", - "@rollup/rollup-win32-x64-msvc": "4.22.5", + "@rollup/rollup-android-arm-eabi": "4.53.2", + "@rollup/rollup-android-arm64": "4.53.2", + "@rollup/rollup-darwin-arm64": "4.53.2", + "@rollup/rollup-darwin-x64": "4.53.2", + "@rollup/rollup-freebsd-arm64": "4.53.2", + "@rollup/rollup-freebsd-x64": "4.53.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.2", + "@rollup/rollup-linux-arm-musleabihf": "4.53.2", + "@rollup/rollup-linux-arm64-gnu": "4.53.2", + "@rollup/rollup-linux-arm64-musl": "4.53.2", + "@rollup/rollup-linux-loong64-gnu": "4.53.2", + "@rollup/rollup-linux-ppc64-gnu": "4.53.2", + "@rollup/rollup-linux-riscv64-gnu": "4.53.2", + "@rollup/rollup-linux-riscv64-musl": "4.53.2", + "@rollup/rollup-linux-s390x-gnu": "4.53.2", + "@rollup/rollup-linux-x64-gnu": "4.53.2", + "@rollup/rollup-linux-x64-musl": "4.53.2", + "@rollup/rollup-openharmony-arm64": "4.53.2", + "@rollup/rollup-win32-arm64-msvc": "4.53.2", + "@rollup/rollup-win32-ia32-msvc": "4.53.2", + "@rollup/rollup-win32-x64-gnu": "4.53.2", + "@rollup/rollup-win32-x64-msvc": "4.53.2", "fsevents": "~2.3.2" } }, @@ -5929,19 +6027,6 @@ "node": ">=16 || 14 >=14.17" } }, - "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -6011,13 +6096,6 @@ "node": ">=14.0.0" } }, - "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, - "license": "MIT" - }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -6039,14 +6117,52 @@ "node": ">=0.8" } }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", "engines": { - "node": ">=4" + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/to-regex-range": { @@ -6259,9 +6375,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", - "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", + "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", "dev": true, "funding": [ { @@ -6279,8 +6395,8 @@ ], "license": "MIT", "dependencies": { - "escalade": "^3.1.2", - "picocolors": "^1.0.1" + "escalade": "^3.2.0", + "picocolors": "^1.1.1" }, "bin": { "update-browserslist-db": "cli.js" @@ -6357,21 +6473,24 @@ } }, "node_modules/vite": { - "version": "5.4.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.6.tgz", - "integrity": "sha512-IeL5f8OO5nylsgzd9tq4qD2QqI0k2CQLGrWD0rCN0EQJZpBK5vJAx0I+GDkMOXxQX/OfFHMuLIx6ddAxGX/k+Q==", + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz", + "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" + "esbuild": "^0.25.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^20.19.0 || >=22.12.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -6380,19 +6499,25 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" }, "peerDependenciesMeta": { "@types/node": { "optional": true }, + "jiti": { + "optional": true + }, "less": { "optional": true }, @@ -6413,9 +6538,46 @@ }, "terser": { "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true } } }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/web-namespaces": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", diff --git a/package.json b/package.json index 946dd15..c836017 100644 --- a/package.json +++ b/package.json @@ -18,13 +18,14 @@ "clsx": "^2.1.1", "framer-motion": "^11.9.0", "highlight.js": "^11.11.1", - "monaco-editor": "^0.54.0", + "monaco-editor": "^0.53.0", "postcss": "^8.4.47", "react": "^18.3.1", "react-dom": "^18.3.1", "react-markdown": "^10.1.0", "react-redux": "^9.2.0", "react-router-dom": "^7.9.4", + "react-toastify": "^11.0.5", "rehype-highlight": "^7.0.2", "rehype-raw": "^7.0.0", "rehype-sanitize": "^6.0.0", @@ -45,6 +46,6 @@ "globals": "^15.9.0", "typescript": "^5.5.3", "typescript-eslint": "^8.0.1", - "vite": "^5.4.1" + "vite": "^7.2.2" } } diff --git a/src/App.tsx b/src/App.tsx index 6d0ec91..009f229 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,18 +8,28 @@ import Home from './pages/Home'; import Mission from './pages/Mission'; import ArticleEditor from './pages/ArticleEditor'; import Article from './pages/Article'; +import ContestEditor from './pages/ContestEditor'; +import ProtectedRoute from './components/router/ProtectedRoute'; function App() { return (
+ }> + } + /> + } + /> + + } /> } /> - } - /> + } /> } /> diff --git a/src/assets/icons/filters/filters-active.svg b/src/assets/icons/filters/filters-active.svg new file mode 100644 index 0000000..4c120fa --- /dev/null +++ b/src/assets/icons/filters/filters-active.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/filters/filters.svg b/src/assets/icons/filters/filters.svg new file mode 100644 index 0000000..00b357b --- /dev/null +++ b/src/assets/icons/filters/filters.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/filters/index.ts b/src/assets/icons/filters/index.ts new file mode 100644 index 0000000..4a4b8a6 --- /dev/null +++ b/src/assets/icons/filters/index.ts @@ -0,0 +1,7 @@ +import iconFilterActive from './filters-active.svg'; +import iconFilter from './filters.svg'; +import iconSort from './sort.svg'; +import iconSortActive from './sort-active.svg'; +import iconSearch from './search.svg'; + +export { iconFilter, iconFilterActive, iconSort, iconSortActive, iconSearch }; diff --git a/src/assets/icons/filters/search.svg b/src/assets/icons/filters/search.svg new file mode 100644 index 0000000..7827a6e --- /dev/null +++ b/src/assets/icons/filters/search.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/filters/sort-active.svg b/src/assets/icons/filters/sort-active.svg new file mode 100644 index 0000000..971008b --- /dev/null +++ b/src/assets/icons/filters/sort-active.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/filters/sort.svg b/src/assets/icons/filters/sort.svg new file mode 100644 index 0000000..5035447 --- /dev/null +++ b/src/assets/icons/filters/sort.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/group/cup.svg b/src/assets/icons/group/cup.svg new file mode 100644 index 0000000..65b15e5 --- /dev/null +++ b/src/assets/icons/group/cup.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/group/home.svg b/src/assets/icons/group/home.svg new file mode 100644 index 0000000..76e7b28 --- /dev/null +++ b/src/assets/icons/group/home.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/group/index.ts b/src/assets/icons/group/index.ts new file mode 100644 index 0000000..1faa753 --- /dev/null +++ b/src/assets/icons/group/index.ts @@ -0,0 +1,5 @@ +import Cup from './cup.svg'; +import Home from './home.svg'; +import MessageChat from './message-chat.svg'; + +export { Cup, MessageChat, Home }; diff --git a/src/assets/icons/group/message-chat.svg b/src/assets/icons/group/message-chat.svg new file mode 100644 index 0000000..1e32098 --- /dev/null +++ b/src/assets/icons/group/message-chat.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/logos/LogoFASIE.png b/src/assets/logos/LogoFASIE.png new file mode 100644 index 0000000..44ee9ab Binary files /dev/null and b/src/assets/logos/LogoFASIE.png differ diff --git a/src/assets/logos/index.ts b/src/assets/logos/index.ts index 5ea6c09..c01722d 100644 --- a/src/assets/logos/index.ts +++ b/src/assets/logos/index.ts @@ -1,3 +1,4 @@ import Logo from './Logo.svg'; +import LogoFASIE from './LogoFASIE.png'; -export { Logo }; +export { Logo, LogoFASIE }; diff --git a/src/components/button/PrimaryButton.tsx b/src/components/button/PrimaryButton.tsx index 6a9423f..415ad1f 100644 --- a/src/components/button/PrimaryButton.tsx +++ b/src/components/button/PrimaryButton.tsx @@ -5,7 +5,7 @@ interface ButtonProps { disabled?: boolean; text?: string; className?: string; - onClick: (e: React.MouseEvent) => void; + onClick: () => void; children?: React.ReactNode; color?: 'primary' | 'secondary' | 'error' | 'warning' | 'success'; } @@ -41,6 +41,9 @@ export const PrimaryButton: React.FC = ({ disabled && 'pointer-events-none', className, )} + onClick={(e) => { + e.stopPropagation(); + }} > {/* Основной контейнер, */}
= ({ '[&:focus-visible+*]:outline-liquid-brightmain', )} disabled={disabled} - onClick={( - e: React.MouseEvent, - ) => { - onClick(e); + onClick={() => { + onClick(); }} /> diff --git a/src/components/button/ReverseButton.tsx b/src/components/button/ReverseButton.tsx index 67ddceb..142924a 100644 --- a/src/components/button/ReverseButton.tsx +++ b/src/components/button/ReverseButton.tsx @@ -5,7 +5,7 @@ interface ButtonProps { disabled?: boolean; text?: string; className?: string; - onClick: (e: React.MouseEvent) => void; + onClick: () => void; children?: React.ReactNode; color?: 'primary' | 'secondary' | 'error' | 'warning' | 'success'; } @@ -41,6 +41,9 @@ export const ReverseButton: React.FC = ({ disabled && 'pointer-events-none', className, )} + onClick={(e) => { + e.stopPropagation(); + }} > {/* Основной контейнер, */}
= ({ '[&:focus-visible+*]:outline-liquid-brightmain', )} disabled={disabled} - onClick={( - e: React.MouseEvent, - ) => { - onClick(e); + onClick={() => { + onClick(); }} /> diff --git a/src/components/button/SecondaryButton.tsx b/src/components/button/SecondaryButton.tsx index e71ab94..bbb2f36 100644 --- a/src/components/button/SecondaryButton.tsx +++ b/src/components/button/SecondaryButton.tsx @@ -5,7 +5,7 @@ interface ButtonProps { disabled?: boolean; text?: string; className?: string; - onClick: (e: React.MouseEvent) => void; + onClick: () => void; children?: React.ReactNode; } @@ -23,6 +23,9 @@ export const SecondaryButton: React.FC = ({ disabled && 'pointer-events-none', className, )} + onClick={(e) => { + e.stopPropagation(); + }} > {/* Основной контейнер, */}
= ({ '[&:focus-visible+*]:outline-liquid-brightmain', )} disabled={disabled} - onClick={(e) => { - onClick(e); + onClick={() => { + onClick(); }} /> diff --git a/src/components/drop-down-list/Filter.tsx b/src/components/drop-down-list/Filter.tsx new file mode 100644 index 0000000..5ef647f --- /dev/null +++ b/src/components/drop-down-list/Filter.tsx @@ -0,0 +1,125 @@ +import React from 'react'; +import { cn } from '../../lib/cn'; +import { checkMark } from '../../assets/icons/input'; +import { useClickOutside } from '../../hooks/useClickOutside'; +import { iconFilter, iconFilterActive } from '../../assets/icons/filters'; + +export interface FilterItem { + text: string; + value: string; +} + +interface FilterProps { + disabled?: boolean; + className?: string; + onChange: (items: FilterItem[]) => void; + defaultState?: FilterItem[]; + items: FilterItem[]; +} + +export const FilterDropDown: React.FC = ({ + disabled = false, + className = '', + onChange, + defaultState = [], + items = [], +}) => { + const [value, setValue] = React.useState(defaultState); + const [active, setActive] = React.useState(false); + + const ref = React.useRef(null); + + useClickOutside(ref, () => { + setActive(false); + }); + + React.useEffect(() => { + onChange(value); + }, [value]); + + const toggleItem = (item: FilterItem) => { + const exists = value.some((val) => val.value === item.value); + if (exists) { + setValue(value.filter((val) => val.value !== item.value)); + } else { + setValue([...value, item]); + } + }; + + return ( +
+
0) && + 'w-fit border-liquid-brightmain border-[1px] border-solid', + )} + onClick={() => { + if (!disabled) setActive(!active); + }} + > +
+ {value.length} +
+
+ + {/* Filter icons */} + + 0) && 'opacity-100', + )} + /> + + {/* Dropdown */} +
+
+
+ {items.map((v) => { + const selected = value.some( + (val) => val.value === v.value, + ); + return ( +
toggleItem(v)} + > + {v.text} + {selected && ( + + )} +
+ ); + })} +
+
+
+
+ ); +}; diff --git a/src/components/drop-down-list/Sorter.tsx b/src/components/drop-down-list/Sorter.tsx new file mode 100644 index 0000000..49ca751 --- /dev/null +++ b/src/components/drop-down-list/Sorter.tsx @@ -0,0 +1,129 @@ +import { FC, useEffect, useRef, useState } from 'react'; +import { cn } from '../../lib/cn'; +import { checkMark } from '../../assets/icons/input'; +import { useClickOutside } from '../../hooks/useClickOutside'; +import { iconSort, iconSortActive } from '../../assets/icons/filters'; + +export interface SorterItem { + text: string; + value: string; +} + +interface SorterProps { + disabled?: boolean; + className?: string; + onChange: (state: string) => void; + defaultState?: SorterItem; + items: SorterItem[]; +} + +export const SorterDropDown: FC = ({ + // disabled = false, + className = '', + onChange, + defaultState, + items = [{ text: '', value: '' }], +}) => { + if (items.length == 0) items.push({ text: '', value: '' }); + + const [value, setValue] = useState( + defaultState != undefined ? defaultState : items[0], + ); + const [active, setActive] = useState(false); + const [activate, setActivate] = useState(false); + + useEffect(() => onChange(value.value), [value]); + + const ref = useRef(null); + + useClickOutside(ref, () => { + setActive(false); + }); + + return ( +
+
{ + setActive(!active); + }} + > +
+ {' '} + {value.text} +
+
+ + + + +
+
+
+ {items.map((v, i) => ( +
{ + setValue(v); + setActive(false); + setActivate(true); + }} + > + {v.text} + + {v.text == value.text && ( + + )} +
+ ))} +
+
+
+
+ ); +}; diff --git a/src/components/input/DateRangeInput.tsx b/src/components/input/DateRangeInput.tsx index 1a1c732..be5b2d5 100644 --- a/src/components/input/DateRangeInput.tsx +++ b/src/components/input/DateRangeInput.tsx @@ -27,7 +27,7 @@ const DateRangeInput: React.FC = ({ type="datetime-local" value={startValue} onChange={(e) => onChange('startsAt', e.target.value)} - className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" + className="mt-1 block w-full rounded-md border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" />
@@ -38,7 +38,7 @@ const DateRangeInput: React.FC = ({ type="datetime-local" value={endValue} onChange={(e) => onChange('endsAt', e.target.value)} - className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" + className="mt-1 block w-full rounded-md border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" />
diff --git a/src/components/input/SearchInput.tsx b/src/components/input/SearchInput.tsx new file mode 100644 index 0000000..0e23057 --- /dev/null +++ b/src/components/input/SearchInput.tsx @@ -0,0 +1,62 @@ +import React from 'react'; +import { cn } from '../../lib/cn'; +import { iconSearch } from '../../assets/icons/filters'; + +interface searchInputProps { + name?: string; + error?: string; + disabled?: boolean; + required?: boolean; + label?: string; + placeholder?: string; + className?: string; + onChange: (state: string) => void; + defaultState?: string; + autocomplete?: string; + onKeyDown?: (e: React.KeyboardEvent) => void; +} + +export const SearchInput: React.FC = ({ + placeholder = '', + className = '', + onChange, + defaultState = '', + name = '', + autocomplete = '', + onKeyDown, +}) => { + const [value, setValue] = React.useState(defaultState); + + React.useEffect(() => onChange(value), [value]); + React.useEffect(() => setValue(defaultState), [defaultState]); + + return ( + + ); +}; diff --git a/src/components/router/ProtectedRoute.tsx b/src/components/router/ProtectedRoute.tsx index 775a461..b704bf7 100644 --- a/src/components/router/ProtectedRoute.tsx +++ b/src/components/router/ProtectedRoute.tsx @@ -1,11 +1,13 @@ // src/routes/ProtectedRoute.tsx -import { Navigate, Outlet } from 'react-router-dom'; +import { Navigate, Outlet, useLocation } from 'react-router-dom'; import { useAppSelector } from '../../redux/hooks'; export default function ProtectedRoute() { const isAuthenticated = useAppSelector((state) => !!state.auth.jwt); + const location = useLocation(); + if (!isAuthenticated) { - return ; + return ; } return ; diff --git a/src/lib/toastNotification.ts b/src/lib/toastNotification.ts new file mode 100644 index 0000000..6c05a93 --- /dev/null +++ b/src/lib/toastNotification.ts @@ -0,0 +1,34 @@ +import { toast } from 'react-toastify'; + +export const toastSuccess = (mes: string, autoClose: number = 3000) => { + toast.success(mes, { + position: 'top-right', + autoClose: autoClose, + hideProgressBar: false, + closeOnClick: true, + pauseOnHover: true, + draggable: true, + }); +}; + +export const toastWarning = (mes: string, autoClose: number = 3000) => { + toast.warning(mes, { + position: 'top-right', + autoClose: autoClose, + hideProgressBar: false, + closeOnClick: true, + pauseOnHover: true, + draggable: true, + }); +}; + +export const toastError = (mes: string, autoClose: number = 3000) => { + toast.error(mes, { + position: 'top-right', + autoClose: autoClose, + hideProgressBar: false, + closeOnClick: true, + pauseOnHover: true, + draggable: true, + }); +}; diff --git a/src/main.tsx b/src/main.tsx index 5e19935..9e1bc2b 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -6,11 +6,13 @@ import './styles/palette/theme-light.css'; import { BrowserRouter } from 'react-router-dom'; import { Provider } from 'react-redux'; import { store } from './redux/store'; +import { ToastContainer } from 'react-toastify'; createRoot(document.getElementById('root')!).render( + , ); diff --git a/src/pages/Article.tsx b/src/pages/Article.tsx index 44b910a..3282ab0 100644 --- a/src/pages/Article.tsx +++ b/src/pages/Article.tsx @@ -5,6 +5,7 @@ import { useEffect } from 'react'; import { fetchArticleById } from '../redux/slices/articles'; import MarkdownPreview from '../views/articleeditor/MarckDownPreview'; import { useQuery } from '../hooks/useQuery'; +import { ArticlesRightPanel } from '../views/home/rightpanel/Articles'; const Article = () => { // Получаем параметры из URL @@ -19,8 +20,12 @@ const Article = () => { return ; } const dispatch = useAppDispatch(); - const article = useAppSelector((state) => state.articles.currentArticle); - const status = useAppSelector((state) => state.articles.statuses.fetchById); + const article = useAppSelector( + (state) => state.articles.fetchArticleById.article, + ); + const status = useAppSelector( + (state) => state.articles.fetchArticleById.status, + ); useEffect(() => { dispatch(fetchArticleById(articleIdNumber)); @@ -65,7 +70,7 @@ const Article = () => { )}
-
+
); }; diff --git a/src/pages/ArticleEditor.tsx b/src/pages/ArticleEditor.tsx index 79002b8..8461e02 100644 --- a/src/pages/ArticleEditor.tsx +++ b/src/pages/ArticleEditor.tsx @@ -23,26 +23,33 @@ const ArticleEditor = () => { const query = useQuery(); const back = query.get('back') ?? undefined; const articleId = Number(query.get('articleId') ?? undefined); - const article = useAppSelector((state) => state.articles.currentArticle); - const refactor = articleId != undefined && !isNaN(articleId); + const refactor = articleId && !isNaN(articleId); + // Достаём данные из redux + const article = useAppSelector( + (state) => state.articles.fetchArticleById.article, + ); + + const statusCreate = useAppSelector( + (state) => state.articles.createArticle.status, + ); + const statusUpdate = useAppSelector( + (state) => state.articles.updateArticle.status, + ); + const statusDelete = useAppSelector( + (state) => state.articles.deleteArticle.status, + ); + + // Локальные состояния const [code, setCode] = useState(article?.content || ''); const [name, setName] = useState(article?.name || ''); const [tagInput, setTagInput] = useState(''); const [tags, setTags] = useState(article?.tags || []); - const [activeEditor, setActiveEditor] = useState(false); - const statusCreate = useAppSelector( - (state) => state.articles.statuses.create, - ); - const statusUpdate = useAppSelector( - (state) => state.articles.statuses.update, - ); - const statusDelete = useAppSelector( - (state) => state.articles.statuses.delete, - ); - + // ========================== + // Теги + // ========================== const addTag = () => { const newTag = tagInput.trim(); if (newTag && !tags.includes(newTag)) { @@ -55,53 +62,63 @@ const ArticleEditor = () => { setTags(tags.filter((tag) => tag !== tagToRemove)); }; + // ========================== + // Эффекты по статусам + // ========================== useEffect(() => { - if (statusCreate == 'successful') { - dispatch(setArticlesStatus({ key: 'create', status: 'idle' })); - navigate(back ? back : '/home/articles'); + if (statusCreate === 'successful') { + dispatch( + setArticlesStatus({ key: 'createArticle', status: 'idle' }), + ); + navigate(back ?? '/home/articles'); } }, [statusCreate]); useEffect(() => { - if (statusDelete == 'successful') { - dispatch(setArticlesStatus({ key: 'delete', status: 'idle' })); - navigate(back ? back : '/home/articles'); - } - }, [statusDelete]); - - useEffect(() => { - if (statusUpdate == 'successful') { - dispatch(setArticlesStatus({ key: 'update', status: 'idle' })); - navigate(back ? back : '/home/articles'); + if (statusUpdate === 'successful') { + dispatch( + setArticlesStatus({ key: 'updateArticle', status: 'idle' }), + ); + navigate(back ?? '/home/articles'); } }, [statusUpdate]); + useEffect(() => { + if (statusDelete === 'successful') { + dispatch( + setArticlesStatus({ key: 'deleteArticle', status: 'idle' }), + ); + navigate(back ?? '/home/articles'); + } + }, [statusDelete]); + + // ========================== + // Получение статьи + // ========================== useEffect(() => { if (articleId) { dispatch(fetchArticleById(articleId)); } }, [articleId]); + // Обновление локального состояния после загрузки статьи useEffect(() => { if (article && refactor) { - setCode(article?.content || ''); - setName(article?.name || ''); - setTags(article?.tags || []); + setCode(article.content || ''); + setName(article.name || ''); + setTags(article.tags || []); } }, [article]); + // ========================== + // Рендер + // ========================== return (
{activeEditor ? ( -
{ - setActiveEditor(false); - }} - /> +
setActiveEditor(false)} /> ) : ( -
navigate(back ? back : '/home/articles')} - /> +
navigate(back ?? '/home/articles')} /> )} {activeEditor ? ( @@ -113,6 +130,8 @@ const ArticleEditor = () => { ? `Редактирование статьи: \"${article?.name}\"` : 'Создание статьи'}
+ + {/* Кнопки действий */}
{refactor ? (
@@ -129,16 +148,16 @@ const ArticleEditor = () => { }} text="Обновить" className="mt-[20px]" - disabled={statusUpdate == 'loading'} + disabled={statusUpdate === 'loading'} /> { - dispatch(deleteArticle(articleId)); - }} + onClick={() => + dispatch(deleteArticle(articleId)) + } color="error" text="Удалить" className="mt-[20px]" - disabled={statusDelete == 'loading'} + disabled={statusDelete === 'loading'} />
) : ( @@ -154,11 +173,12 @@ const ArticleEditor = () => { }} text="Опубликовать" className="mt-[20px]" - disabled={statusCreate == 'loading'} + disabled={statusCreate === 'loading'} /> )}
+ {/* Название */} { className="mt-[20px] max-w-[600px]" type="text" label="Название" - onChange={(v) => { - setName(v); - }} + onChange={setName} placeholder="Новая статья" /> - {/* Блок для тегов */} + {/* Теги */}
{ className="mt-[20px] max-w-[600px]" type="text" label="Теги" - onChange={(v) => { - setTagInput(v); - }} + onChange={setTagInput} defaultState={tagInput} placeholder="arrays" onKeyDown={(e) => { - console.log(e.key); - if (e.key == 'Enter') addTag(); + if (e.key === 'Enter') addTag(); }} /> {
+ {/* Просмотр и переход в редактор */} setActiveEditor(true)} text="Редактировать текст" @@ -222,7 +238,7 @@ const ArticleEditor = () => { />
)} diff --git a/src/pages/ContestEditor.tsx b/src/pages/ContestEditor.tsx new file mode 100644 index 0000000..a2abf04 --- /dev/null +++ b/src/pages/ContestEditor.tsx @@ -0,0 +1,368 @@ +import { useEffect, useState } from 'react'; +import Header from '../views/articleeditor/Header'; +import { PrimaryButton } from '../components/button/PrimaryButton'; +import { Input } from '../components/input/Input'; +import { useAppDispatch, useAppSelector } from '../redux/hooks'; +import { + CreateContestBody, + deleteContest, + fetchContestById, + setContestStatus, + updateContest, +} from '../redux/slices/contests'; +import DateRangeInput from '../components/input/DateRangeInput'; +import { useQuery } from '../hooks/useQuery'; +import { Navigate, useNavigate } from 'react-router-dom'; +import { fetchMissionById } from '../redux/slices/missions'; +import { ReverseButton } from '../components/button/ReverseButton'; + +interface Mission { + id: number; + name: string; +} + +/** + * Страница создания / редактирования контеста + */ +const ContestEditor = () => { + const dispatch = useAppDispatch(); + const navigate = useNavigate(); + + const query = useQuery(); + const back = query.get('back') ?? undefined; + const contestId = Number(query.get('contestId') ?? undefined); + const refactor = !!contestId; + + if (!refactor) { + return ; + } + + const status = useAppSelector( + (state) => state.contests.createContest.status, + ); + + const [missionIdInput, setMissionIdInput] = useState(''); + + const [contest, setContest] = useState({ + name: '', + description: '', + scheduleType: 'AlwaysOpen', + visibility: 'Public', + startsAt: '', + endsAt: '', + attemptDurationMinutes: 60, + maxAttempts: 1, + allowEarlyFinish: true, + missionIds: [], + articleIds: [], + }); + + const [missions, setMissions] = useState([]); + + const statusDelete = useAppSelector( + (state) => state.contests.deleteContest.status, + ); + const statusUpdate = useAppSelector( + (state) => state.contests.updateContest.status, + ); + + const { contest: contestById, status: contestByIdstatus } = useAppSelector( + (state) => state.contests.fetchContestById, + ); + useEffect(() => { + if (status === 'successful') { + } + }, [status]); + + const handleChange = (key: keyof CreateContestBody, value: any) => { + setContest((prev) => ({ ...prev, [key]: value })); + }; + + const handleUpdateContest = () => { + dispatch(updateContest({ ...contest, contestId })); + }; + + const handleDeleteContest = () => { + dispatch(deleteContest(contestId)); + }; + + const addMission = () => { + const id = Number(missionIdInput.trim()); + if (!id || contest.missionIds?.includes(id)) return; + dispatch(fetchMissionById(id)) + .unwrap() + .then((mission) => { + setMissions((prev) => [...prev, mission]); + setContest((prev) => ({ + ...prev, + missionIds: [...(prev.missionIds ?? []), id], + })); + setMissionIdInput(''); + }) + .catch((err) => {}); + }; + + const removeMission = (removeId: number) => { + setContest({ + ...contest, + missionIds: contest.missionIds?.filter((v) => v !== removeId), + }); + setMissions(missions.filter((v) => v.id != removeId)); + }; + + useEffect(() => { + if (statusDelete == 'successful') { + dispatch( + setContestStatus({ key: 'deleteContest', status: 'idle' }), + ); + navigate('/home/account/contests'); + } + }, [statusDelete]); + + useEffect(() => { + if (statusUpdate == 'successful') { + dispatch( + setContestStatus({ key: 'updateContest', status: 'idle' }), + ); + navigate('/home/account/contests'); + } + }, [statusUpdate]); + + useEffect(() => { + if (refactor) { + dispatch(fetchContestById(contestId)); + } + }, [refactor]); + + useEffect(() => { + if (refactor && contestByIdstatus == 'successful' && contestById) { + setContest({ + ...contestById, + // groupIds: contestById.groups.map(group => group.groupId), + missionIds: contestById.missions?.map((mission) => mission.id), + articleIds: contestById.articles?.map( + (article) => article.articleId, + ), + visibility: 'Public', + scheduleType: 'AlwaysOpen', + }); + setMissions(contestById.missions ?? []); + } + }, [contestById]); + + return ( +
+
navigate(back || '/home/contests')} /> + +
+ {/* Левая панешь */} +
+
+

+ +
+
+ {refactor + ? `Редактирвоание контеста #${contestId} \"${contestById?.name}\"` + : 'Создать контест'} +
+ + handleChange('name', v)} + defaultState={contest.name ?? ''} + /> + + handleChange('description', v)} + defaultState={contest.description ?? ''} + /> + +
+
+ + +
+ +
+ + +
+
+ + {/* Даты начала и конца */} +
+ +
+ + {/* Продолжительность и лимиты */} +
+ + handleChange( + 'attemptDurationMinutes', + Number(v), + ) + } + /> + + handleChange('maxAttempts', Number(v)) + } + /> +
+ + {/* Разрешить раннее завершение */} +
+ + handleChange( + 'allowEarlyFinish', + e.target.checked, + ) + } + /> + +
+ + {/* Кнопки */} +
+ + +
+
+
+
+ + {/* Правая панель */} +
+
+

+ + {/* Блок для тегов */} +
+
+ { + setMissionIdInput(v); + }} + defaultState={missionIdInput} + placeholder="458" + onKeyDown={(e) => { + if (e.key == 'Enter') addMission(); + }} + /> + +
+
+ {missions.map((v, i) => ( +
+ {v.id} + {v.name} + +
+ ))} +
+
+
+
+
+
+ ); +}; + +export default ContestEditor; diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index 9cebabb..aa588ae 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -1,4 +1,4 @@ -// import React from "react"; +// src/pages/Home.tsx import { Route, Routes } from 'react-router-dom'; import Login from '../views/home/auth/Login'; import Register from '../views/home/auth/Register'; @@ -11,10 +11,19 @@ import Articles from '../views/home/articles/Articles'; import Groups from '../views/home/groups/Groups'; import Contests from '../views/home/contests/Contests'; import { PrimaryButton } from '../components/button/PrimaryButton'; -import Group from '../views/home/groups/Group'; +import Group from '../views/home/group/Group'; import Contest from '../views/home/contest/Contest'; import Account from '../views/home/account/Account'; import ProtectedRoute from '../components/router/ProtectedRoute'; +import { MissionsRightPanel } from '../views/home/rightpanel/Missions'; +import { ArticlesRightPanel } from '../views/home/rightpanel/Articles'; +import { GroupRightPanel } from '../views/home/rightpanel/Group'; +import GroupInvite from '../views/home/groupinviter/GroupInvite'; +import { + toastError, + toastSuccess, + toastWarning, +} from '../lib/toastNotification'; const Home = () => { const name = useAppSelector((state) => state.auth.username); @@ -34,14 +43,18 @@ const Home = () => { }> } /> + } + /> + } /> + } /> } /> } /> } /> } /> - } /> - } /> } /> } /> {

{jwt}

{ - if (jwt) + if (jwt) { navigator.clipboard.writeText(jwt); + alert(jwt); + } }} text="скопировать токен" className="pt-[20px]" @@ -65,6 +80,30 @@ const Home = () => { > выйти + +
+ { + toastSuccess('Success'); + }} + /> + { + toastWarning('Warning'); + }} + /> + { + toastError('Error'); + }} + /> +
} /> @@ -72,7 +111,12 @@ const Home = () => { { - } /> + } /> + } /> + } + /> } diff --git a/src/pages/Mission.tsx b/src/pages/Mission.tsx index b09c25a..0a6435b 100644 --- a/src/pages/Mission.tsx +++ b/src/pages/Mission.tsx @@ -20,6 +20,7 @@ const Mission = () => { const query = useQuery(); const back = query.get('back') ?? undefined; + const contestId = Number(query.get('contestId') ?? undefined); if (!missionId || isNaN(missionIdNumber)) { if (back) return ; @@ -148,9 +149,7 @@ const Mission = () => { html: htmlStatement.statementTexts['problem.html'], mediaFiles: latexStatement.mediaFiles, }; - } catch (err) { - console.error('Ошибка парсинга statementTexts:', err); - } + } catch (err) {} return (
@@ -185,7 +184,7 @@ const Mission = () => { language: language, languageVersion: 'latest', sourceCode: code, - contestId: null, + contestId: contestId, }), ).unwrap(); dispatch( @@ -198,7 +197,10 @@ const Mission = () => {
- +
diff --git a/src/redux/slices/account.ts b/src/redux/slices/account.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/redux/slices/articles.ts b/src/redux/slices/articles.ts index 73c59df..e2346dc 100644 --- a/src/redux/slices/articles.ts +++ b/src/redux/slices/articles.ts @@ -1,7 +1,9 @@ import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; import axios from '../../axios'; -// ─── Типы ──────────────────────────────────────────── +// ===================== +// Типы +// ===================== type Status = 'idle' | 'loading' | 'successful' | 'failed'; @@ -15,39 +17,145 @@ export interface Article { updatedAt: string; } -interface ArticlesState { - articles: Article[]; - currentArticle?: Article; +interface ArticlesResponse { hasNextPage: boolean; - statuses: { - create: Status; - update: Status; - delete: Status; - fetchAll: Status; - fetchById: Status; + articles: Article[]; +} + +// ===================== +// Состояние +// ===================== + +interface ArticlesState { + fetchArticles: { + articles: Article[]; + hasNextPage: boolean; + status: Status; + error?: string; + }; + fetchArticleById: { + article?: Article; + status: Status; + error?: string; + }; + createArticle: { + article?: Article; + status: Status; + error?: string; + }; + updateArticle: { + article?: Article; + status: Status; + error?: string; + }; + deleteArticle: { + status: Status; + error?: string; + }; + fetchMyArticles: { + articles: Article[]; + status: Status; + error?: string; }; - error: string | null; } const initialState: ArticlesState = { - articles: [], - currentArticle: undefined, - hasNextPage: false, - statuses: { - create: 'idle', - update: 'idle', - delete: 'idle', - fetchAll: 'idle', - fetchById: 'idle', + fetchArticles: { + articles: [], + hasNextPage: false, + status: 'idle', + error: undefined, + }, + fetchArticleById: { + article: undefined, + status: 'idle', + error: undefined, + }, + createArticle: { + article: undefined, + status: 'idle', + error: undefined, + }, + updateArticle: { + article: undefined, + status: 'idle', + error: undefined, + }, + deleteArticle: { + status: 'idle', + error: undefined, + }, + fetchMyArticles: { + articles: [], + status: 'idle', + error: undefined, }, - error: null, }; -// ─── Async Thunks ───────────────────────────────────── +// ===================== +// Async Thunks +// ===================== -// POST /articles +// Все статьи +export const fetchArticles = createAsyncThunk( + 'articles/fetchArticles', + async ( + { + page = 0, + pageSize = 10, + tags, + }: { page?: number; pageSize?: number; tags?: string[] } = {}, + { rejectWithValue }, + ) => { + try { + const params: any = { page, pageSize }; + if (tags && tags.length > 0) params.tags = tags; + const response = await axios.get('/articles', { + params, + }); + return response.data; + } catch (err: any) { + return rejectWithValue( + err.response?.data?.message || 'Ошибка при получении статей', + ); + } + }, +); + +// Мои статьи +export const fetchMyArticles = createAsyncThunk( + 'articles/fetchMyArticles', + async (_, { rejectWithValue }) => { + try { + const response = await axios.get('/articles/my'); + return response.data; + } catch (err: any) { + return rejectWithValue( + err.response?.data?.message || + 'Ошибка при получении моих статей', + ); + } + }, +); + +// Статья по ID +export const fetchArticleById = createAsyncThunk( + 'articles/fetchById', + async (articleId: number, { rejectWithValue }) => { + try { + const response = await axios.get
(`/articles/${articleId}`); + return response.data; + } catch (err: any) { + return rejectWithValue( + err.response?.data?.message || 'Ошибка при получении статьи', + ); + } + }, +); + +// Создание статьи export const createArticle = createAsyncThunk( - 'articles/createArticle', + 'articles/create', async ( { name, @@ -57,12 +165,12 @@ export const createArticle = createAsyncThunk( { rejectWithValue }, ) => { try { - const response = await axios.post('/articles', { + const response = await axios.post
('/articles', { name, content, tags, }); - return response.data as Article; + return response.data; } catch (err: any) { return rejectWithValue( err.response?.data?.message || 'Ошибка при создании статьи', @@ -71,9 +179,9 @@ export const createArticle = createAsyncThunk( }, ); -// PUT /articles/{articleId} +// Обновление статьи export const updateArticle = createAsyncThunk( - 'articles/updateArticle', + 'articles/update', async ( { articleId, @@ -84,12 +192,15 @@ export const updateArticle = createAsyncThunk( { rejectWithValue }, ) => { try { - const response = await axios.put(`/articles/${articleId}`, { - name, - content, - tags, - }); - return response.data as Article; + const response = await axios.put
( + `/articles/${articleId}`, + { + name, + content, + tags, + }, + ); + return response.data; } catch (err: any) { return rejectWithValue( err.response?.data?.message || 'Ошибка при обновлении статьи', @@ -98,9 +209,9 @@ export const updateArticle = createAsyncThunk( }, ); -// DELETE /articles/{articleId} +// Удаление статьи export const deleteArticle = createAsyncThunk( - 'articles/deleteArticle', + 'articles/delete', async (articleId: number, { rejectWithValue }) => { try { await axios.delete(`/articles/${articleId}`); @@ -113,186 +224,136 @@ export const deleteArticle = createAsyncThunk( }, ); -// GET /articles -export const fetchArticles = createAsyncThunk( - 'articles/fetchArticles', - async ( - { - page = 0, - pageSize = 10, - tags, - }: { page?: number; pageSize?: number; tags?: string[] }, - { rejectWithValue }, - ) => { - try { - const params: any = { page, pageSize }; - if (tags && tags.length > 0) params.tags = tags; - const response = await axios.get('/articles', { params }); - return response.data as { - hasNextPage: boolean; - articles: Article[]; - }; - } catch (err: any) { - return rejectWithValue( - err.response?.data?.message || 'Ошибка при получении статей', - ); - } - }, -); - -// GET /articles/{articleId} -export const fetchArticleById = createAsyncThunk( - 'articles/fetchArticleById', - async (articleId: number, { rejectWithValue }) => { - try { - const response = await axios.get(`/articles/${articleId}`); - return response.data as Article; - } catch (err: any) { - return rejectWithValue( - err.response?.data?.message || 'Ошибка при получении статьи', - ); - } - }, -); - -// ─── Slice ──────────────────────────────────────────── +// ===================== +// Slice +// ===================== const articlesSlice = createSlice({ name: 'articles', initialState, reducers: { - clearCurrentArticle: (state) => { - state.currentArticle = undefined; - }, setArticlesStatus: ( state, - action: PayloadAction<{ - key: keyof ArticlesState['statuses']; - status: Status; - }>, + action: PayloadAction<{ key: keyof ArticlesState; status: Status }>, ) => { const { key, status } = action.payload; - state.statuses[key] = status; + if (state[key]) { + (state[key] as any).status = status; + } }, }, extraReducers: (builder) => { - // ─── CREATE ARTICLE ─── - builder.addCase(createArticle.pending, (state) => { - state.statuses.create = 'loading'; - state.error = null; - }); - builder.addCase( - createArticle.fulfilled, - (state, action: PayloadAction
) => { - state.statuses.create = 'successful'; - state.articles.push(action.payload); - }, - ); - builder.addCase( - createArticle.rejected, - (state, action: PayloadAction) => { - state.statuses.create = 'failed'; - state.error = action.payload; - }, - ); - - // ─── UPDATE ARTICLE ─── - builder.addCase(updateArticle.pending, (state) => { - state.statuses.update = 'loading'; - state.error = null; - }); - builder.addCase( - updateArticle.fulfilled, - (state, action: PayloadAction
) => { - state.statuses.update = 'successful'; - const index = state.articles.findIndex( - (a) => a.id === action.payload.id, - ); - if (index !== -1) state.articles[index] = action.payload; - if (state.currentArticle?.id === action.payload.id) - state.currentArticle = action.payload; - }, - ); - builder.addCase( - updateArticle.rejected, - (state, action: PayloadAction) => { - state.statuses.update = 'failed'; - state.error = action.payload; - }, - ); - - // ─── DELETE ARTICLE ─── - builder.addCase(deleteArticle.pending, (state) => { - state.statuses.delete = 'loading'; - state.error = null; - }); - builder.addCase( - deleteArticle.fulfilled, - (state, action: PayloadAction) => { - state.statuses.delete = 'successful'; - state.articles = state.articles.filter( - (a) => a.id !== action.payload, - ); - if (state.currentArticle?.id === action.payload) - state.currentArticle = undefined; - }, - ); - builder.addCase( - deleteArticle.rejected, - (state, action: PayloadAction) => { - state.statuses.delete = 'failed'; - state.error = action.payload; - }, - ); - - // ─── FETCH ARTICLES ─── + // fetchArticles builder.addCase(fetchArticles.pending, (state) => { - state.statuses.fetchAll = 'loading'; - state.error = null; + state.fetchArticles.status = 'loading'; + state.fetchArticles.error = undefined; }); builder.addCase( fetchArticles.fulfilled, - ( - state, - action: PayloadAction<{ - hasNextPage: boolean; - articles: Article[]; - }>, - ) => { - state.statuses.fetchAll = 'successful'; - state.articles = action.payload.articles; - state.hasNextPage = action.payload.hasNextPage; - }, - ); - builder.addCase( - fetchArticles.rejected, - (state, action: PayloadAction) => { - state.statuses.fetchAll = 'failed'; - state.error = action.payload; + (state, action: PayloadAction) => { + state.fetchArticles.status = 'successful'; + state.fetchArticles.articles = action.payload.articles; + state.fetchArticles.hasNextPage = action.payload.hasNextPage; }, ); + builder.addCase(fetchArticles.rejected, (state, action: any) => { + state.fetchArticles.status = 'failed'; + state.fetchArticles.error = action.payload; + }); - // ─── FETCH ARTICLE BY ID ─── + // fetchMyArticles + builder.addCase(fetchMyArticles.pending, (state) => { + state.fetchMyArticles.status = 'loading'; + state.fetchMyArticles.error = undefined; + }); + builder.addCase( + fetchMyArticles.fulfilled, + (state, action: PayloadAction) => { + state.fetchMyArticles.status = 'successful'; + state.fetchMyArticles.articles = action.payload; + }, + ); + builder.addCase(fetchMyArticles.rejected, (state, action: any) => { + state.fetchMyArticles.status = 'failed'; + state.fetchMyArticles.error = action.payload; + }); + + // fetchArticleById builder.addCase(fetchArticleById.pending, (state) => { - state.statuses.fetchById = 'loading'; - state.error = null; + state.fetchArticleById.status = 'loading'; + state.fetchArticleById.error = undefined; }); builder.addCase( fetchArticleById.fulfilled, (state, action: PayloadAction
) => { - state.statuses.fetchById = 'successful'; - state.currentArticle = action.payload; + state.fetchArticleById.status = 'successful'; + state.fetchArticleById.article = action.payload; }, ); + builder.addCase(fetchArticleById.rejected, (state, action: any) => { + state.fetchArticleById.status = 'failed'; + state.fetchArticleById.error = action.payload; + }); + + // createArticle + builder.addCase(createArticle.pending, (state) => { + state.createArticle.status = 'loading'; + state.createArticle.error = undefined; + }); builder.addCase( - fetchArticleById.rejected, - (state, action: PayloadAction) => { - state.statuses.fetchById = 'failed'; - state.error = action.payload; + createArticle.fulfilled, + (state, action: PayloadAction
) => { + state.createArticle.status = 'successful'; + state.createArticle.article = action.payload; }, ); + builder.addCase(createArticle.rejected, (state, action: any) => { + state.createArticle.status = 'failed'; + state.createArticle.error = action.payload; + }); + + // updateArticle + builder.addCase(updateArticle.pending, (state) => { + state.updateArticle.status = 'loading'; + state.updateArticle.error = undefined; + }); + builder.addCase( + updateArticle.fulfilled, + (state, action: PayloadAction
) => { + state.updateArticle.status = 'successful'; + state.updateArticle.article = action.payload; + }, + ); + builder.addCase(updateArticle.rejected, (state, action: any) => { + state.updateArticle.status = 'failed'; + state.updateArticle.error = action.payload; + }); + + // deleteArticle + builder.addCase(deleteArticle.pending, (state) => { + state.deleteArticle.status = 'loading'; + state.deleteArticle.error = undefined; + }); + builder.addCase( + deleteArticle.fulfilled, + (state, action: PayloadAction) => { + state.deleteArticle.status = 'successful'; + state.fetchArticles.articles = + state.fetchArticles.articles.filter( + (a) => a.id !== action.payload, + ); + state.fetchMyArticles.articles = + state.fetchMyArticles.articles.filter( + (a) => a.id !== action.payload, + ); + }, + ); + builder.addCase(deleteArticle.rejected, (state, action: any) => { + state.deleteArticle.status = 'failed'; + state.deleteArticle.error = action.payload; + }); }, }); -export const { clearCurrentArticle, setArticlesStatus } = articlesSlice.actions; +export const { setArticlesStatus } = articlesSlice.actions; export const articlesReducer = articlesSlice.reducer; diff --git a/src/redux/slices/auth.ts b/src/redux/slices/auth.ts index 9b04ff6..eeabf20 100644 --- a/src/redux/slices/auth.ts +++ b/src/redux/slices/auth.ts @@ -121,11 +121,26 @@ export const refreshToken = createAsyncThunk( export const fetchWhoAmI = createAsyncThunk( 'auth/whoami', - async (_, { rejectWithValue }) => { + async (_, { dispatch, getState, rejectWithValue }) => { try { const response = await axios.get('/authentication/whoami'); return response.data; } catch (err: any) { + const state: any = getState(); + const refresh = state.auth.refreshToken; + + if (refresh) { + // пробуем refresh + const result = await dispatch( + refreshToken({ refreshToken: refresh }), + ); + + // если успешный, повторить whoami + if (refreshToken.fulfilled.match(result)) { + const retry = await axios.get('/authentication/whoami'); + return retry.data; + } + } return rejectWithValue( err.response?.data?.message || 'Failed to fetch user info', ); @@ -269,6 +284,22 @@ const authSlice = createSlice({ builder.addCase(fetchWhoAmI.rejected, (state, action) => { state.status = 'failed'; state.error = action.payload as string; + + // Если пользователь не авторизован (401), делаем logout и пытаемся refresh + if ( + action.payload === 'Unauthorized' || + action.payload === 'Failed to fetch user info' + ) { + // Вызов logout + state.jwt = null; + state.refreshToken = null; + state.username = null; + state.email = null; + state.id = null; + localStorage.removeItem('jwt'); + localStorage.removeItem('refreshToken'); + delete axios.defaults.headers.common['Authorization']; + } }); }, }); diff --git a/src/redux/slices/contests.ts b/src/redux/slices/contests.ts index a5302ec..5ddff69 100644 --- a/src/redux/slices/contests.ts +++ b/src/redux/slices/contests.ts @@ -5,17 +5,43 @@ import axios from '../../axios'; // Типы // ===================== +// ===================== +// Типы для посылок +// ===================== + +export interface Solution { + id: number; + missionId: number; + language: string; + languageVersion: string; + sourceCode: string; + status: string; + time: string; + testerState: string; + testerErrorCode: string; + testerMessage: string; + currentTest: number; + amountOfTests: number; +} + +export interface Submission { + id: number; + userId: number; + solution: Solution; + contestId: number; + contestName: string; + sourceType: string; +} + export interface Mission { id: number; authorId: number; name: string; difficulty: number; tags: string[]; - createdAt: string; - updatedAt: string; timeLimitMilliseconds: number; memoryLimitBytes: number; - statements: null; + statements: string; } export interface Member { @@ -24,21 +50,27 @@ export interface Member { role: string; } +export interface Group { + groupId: number; + groupName: string; +} + export interface Contest { id: number; name: string; - description: string; - scheduleType: string; - startsAt: string; - endsAt: string; - attemptDurationMinutes: number | null; - maxAttempts: number | null; - allowEarlyFinish: boolean | null; - groupId: number | null; - groupName: string | null; - missions: Mission[]; - articles: any[]; - members: Member[]; + description?: string; + scheduleType: 'AlwaysOpen' | 'FixedWindow' | 'RollingWindow'; + visibility: 'Public' | 'GroupPrivate'; + startsAt?: string; + endsAt?: string; + attemptDurationMinutes?: number; + maxAttempts?: number; + allowEarlyFinish?: boolean; + groupId?: number; + groupName?: string; + missions?: Mission[]; + articles?: any[]; + members?: Member[]; } interface ContestsResponse { @@ -47,20 +79,19 @@ interface ContestsResponse { } export interface CreateContestBody { - name?: string | null; - description?: string | null; + name: string; + description?: string; scheduleType: 'AlwaysOpen' | 'FixedWindow' | 'RollingWindow'; visibility: 'Public' | 'GroupPrivate'; - startsAt?: string | null; - endsAt?: string | null; - attemptDurationMinutes?: number | null; - maxAttempts?: number | null; - allowEarlyFinish?: boolean | null; - groupId?: number | null; - missionIds?: number[] | null; - articleIds?: number[] | null; - participantIds?: number[] | null; - organizerIds?: number[] | null; + startsAt?: string; + endsAt?: string; + attemptDurationMinutes?: number; + maxAttempts?: number; + allowEarlyFinish?: boolean; + groupId?: number; + groupName?: string; + missionIds?: number[]; + articleIds?: number[]; } // ===================== @@ -70,33 +101,164 @@ export interface CreateContestBody { type Status = 'idle' | 'loading' | 'successful' | 'failed'; interface ContestsState { - contests: Contest[]; - selectedContest: Contest | null; - hasNextPage: boolean; - statuses: { - fetchList: Status; - fetchById: Status; - create: Status; + fetchContests: { + contests: Contest[]; + hasNextPage: boolean; + status: Status; + error?: string; + }; + fetchContestById: { + contest: Contest; + status: Status; + error?: string; + }; + createContest: { + contest: Contest; + status: Status; + error?: string; + }; + fetchMySubmissions: { + submissions: Submission[]; + status: Status; + error?: string; + }; + updateContest: { + contest: Contest; + status: Status; + error?: string; + }; + deleteContest: { + status: Status; + error?: string; + }; + fetchMyContests: { + contests: Contest[]; + status: Status; + error?: string; + }; + fetchRegisteredContests: { + contests: Contest[]; + hasNextPage: boolean; + status: Status; + error?: string; }; - error: string | null; } const initialState: ContestsState = { - contests: [], - selectedContest: null, - hasNextPage: false, - statuses: { - fetchList: 'idle', - fetchById: 'idle', - create: 'idle', + fetchContests: { + contests: [], + hasNextPage: false, + status: 'idle', + error: undefined, + }, + fetchContestById: { + contest: { + id: 0, + name: '', + description: '', + scheduleType: 'AlwaysOpen', + visibility: 'Public', + startsAt: '', + endsAt: '', + attemptDurationMinutes: 0, + maxAttempts: 0, + allowEarlyFinish: false, + groupId: undefined, + groupName: undefined, + missions: [], + articles: [], + members: [], + }, + status: 'idle', + error: undefined, + }, + fetchMySubmissions: { + submissions: [], + status: 'idle', + error: undefined, + }, + + createContest: { + contest: { + id: 0, + name: '', + description: '', + scheduleType: 'AlwaysOpen', + visibility: 'Public', + startsAt: '', + endsAt: '', + attemptDurationMinutes: 0, + maxAttempts: 0, + allowEarlyFinish: false, + groupId: undefined, + groupName: undefined, + missions: [], + articles: [], + members: [], + }, + status: 'idle', + error: undefined, + }, + updateContest: { + contest: { + id: 0, + name: '', + description: '', + scheduleType: 'AlwaysOpen', + visibility: 'Public', + startsAt: '', + endsAt: '', + attemptDurationMinutes: 0, + maxAttempts: 0, + allowEarlyFinish: false, + groupId: undefined, + groupName: undefined, + missions: [], + articles: [], + members: [], + }, + status: 'idle', + error: undefined, + }, + deleteContest: { + status: 'idle', + error: undefined, + }, + fetchMyContests: { + contests: [], + status: 'idle', + error: undefined, + }, + fetchRegisteredContests: { + contests: [], + hasNextPage: false, + status: 'idle', + error: undefined, }, - error: null, }; // ===================== // Async Thunks // ===================== +// Мои посылки в контесте +export const fetchMySubmissions = createAsyncThunk( + 'contests/fetchMySubmissions', + async (contestId: number, { rejectWithValue }) => { + try { + const response = await axios.get( + `/contests/${contestId}/submissions/my`, + ); + return response.data; + } catch (err: any) { + return rejectWithValue( + err.response?.data?.message || 'Failed to fetch my submissions', + ); + } + }, +); + +// Все контесты export const fetchContests = createAsyncThunk( 'contests/fetchAll', async ( @@ -121,6 +283,7 @@ export const fetchContests = createAsyncThunk( }, ); +// Контест по ID export const fetchContestById = createAsyncThunk( 'contests/fetchById', async (id: number, { rejectWithValue }) => { @@ -135,6 +298,7 @@ export const fetchContestById = createAsyncThunk( }, ); +// Создание контеста export const createContest = createAsyncThunk( 'contests/create', async (contestData: CreateContestBody, { rejectWithValue }) => { @@ -152,6 +316,83 @@ export const createContest = createAsyncThunk( }, ); +// 🆕 Обновление контеста +export const updateContest = createAsyncThunk( + 'contests/update', + async ( + { + contestId, + ...contestData + }: { contestId: number } & CreateContestBody, + { rejectWithValue }, + ) => { + try { + const response = await axios.put( + `/contests/${contestId}`, + contestData, + ); + return response.data; + } catch (err: any) { + return rejectWithValue( + err.response?.data?.message || 'Failed to update contest', + ); + } + }, +); + +// 🆕 Удаление контеста +export const deleteContest = createAsyncThunk( + 'contests/delete', + async (contestId: number, { rejectWithValue }) => { + try { + await axios.delete(`/contests/${contestId}`); + return contestId; + } catch (err: any) { + return rejectWithValue( + err.response?.data?.message || 'Failed to delete contest', + ); + } + }, +); + +// Контесты, созданные мной +export const fetchMyContests = createAsyncThunk( + 'contests/fetchMyContests', + async (_, { rejectWithValue }) => { + try { + const response = await axios.get('/contests/my'); + return response.data; + } catch (err: any) { + return rejectWithValue( + err.response?.data?.message || 'Failed to fetch my contests', + ); + } + }, +); + +// Контесты, где я зарегистрирован +export const fetchRegisteredContests = createAsyncThunk( + 'contests/fetchRegisteredContests', + async ( + params: { page?: number; pageSize?: number } = {}, + { rejectWithValue }, + ) => { + try { + const { page = 0, pageSize = 10 } = params; + const response = await axios.get( + '/contests/registered', + { params: { page, pageSize } }, + ); + return response.data; + } catch (err: any) { + return rejectWithValue( + err.response?.data?.message || + 'Failed to fetch registered contests', + ); + } + }, +); + // ===================== // Slice // ===================== @@ -160,78 +401,166 @@ const contestsSlice = createSlice({ name: 'contests', initialState, reducers: { - clearSelectedContest: (state) => { - state.selectedContest = null; - }, + // 🆕 Сброс статусов setContestStatus: ( state, - action: PayloadAction<{ - key: keyof ContestsState['statuses']; - status: Status; - }>, + action: PayloadAction<{ key: keyof ContestsState; status: Status }>, ) => { - state.statuses[action.payload.key] = action.payload.status; + const { key, status } = action.payload; + if (state[key]) { + (state[key] as any).status = status; + } }, }, extraReducers: (builder) => { + // 🆕 fetchMySubmissions + builder.addCase(fetchMySubmissions.pending, (state) => { + state.fetchMySubmissions.status = 'loading'; + state.fetchMySubmissions.error = undefined; + }); + builder.addCase( + fetchMySubmissions.fulfilled, + (state, action: PayloadAction) => { + state.fetchMySubmissions.status = 'successful'; + state.fetchMySubmissions.submissions = action.payload; + }, + ); + builder.addCase(fetchMySubmissions.rejected, (state, action: any) => { + state.fetchMySubmissions.status = 'failed'; + state.fetchMySubmissions.error = action.payload; + }); + // fetchContests builder.addCase(fetchContests.pending, (state) => { - state.statuses.fetchList = 'loading'; - state.error = null; + state.fetchContests.status = 'loading'; + state.fetchContests.error = undefined; }); builder.addCase( fetchContests.fulfilled, (state, action: PayloadAction) => { - state.statuses.fetchList = 'successful'; - state.contests = action.payload.contests; - state.hasNextPage = action.payload.hasNextPage; - }, - ); - builder.addCase( - fetchContests.rejected, - (state, action: PayloadAction) => { - state.statuses.fetchList = 'failed'; - state.error = action.payload; + state.fetchContests.status = 'successful'; + state.fetchContests.contests = action.payload.contests; + state.fetchContests.hasNextPage = action.payload.hasNextPage; }, ); + builder.addCase(fetchContests.rejected, (state, action: any) => { + state.fetchContests.status = 'failed'; + state.fetchContests.error = action.payload; + }); // fetchContestById builder.addCase(fetchContestById.pending, (state) => { - state.statuses.fetchById = 'loading'; - state.error = null; + state.fetchContestById.status = 'loading'; + state.fetchContestById.error = undefined; }); builder.addCase( fetchContestById.fulfilled, (state, action: PayloadAction) => { - state.statuses.fetchById = 'successful'; - state.selectedContest = action.payload; - }, - ); - builder.addCase( - fetchContestById.rejected, - (state, action: PayloadAction) => { - state.statuses.fetchById = 'failed'; - state.error = action.payload; + state.fetchContestById.status = 'successful'; + state.fetchContestById.contest = action.payload; }, ); + builder.addCase(fetchContestById.rejected, (state, action: any) => { + state.fetchContestById.status = 'failed'; + state.fetchContestById.error = action.payload; + }); // createContest builder.addCase(createContest.pending, (state) => { - state.statuses.create = 'loading'; - state.error = null; + state.createContest.status = 'loading'; + state.createContest.error = undefined; }); builder.addCase( createContest.fulfilled, (state, action: PayloadAction) => { - state.statuses.create = 'successful'; - state.contests.unshift(action.payload); + state.createContest.status = 'successful'; + state.createContest.contest = action.payload; + }, + ); + builder.addCase(createContest.rejected, (state, action: any) => { + state.createContest.status = 'failed'; + state.createContest.error = action.payload; + }); + + // 🆕 updateContest + builder.addCase(updateContest.pending, (state) => { + state.updateContest.status = 'loading'; + state.updateContest.error = undefined; + }); + builder.addCase( + updateContest.fulfilled, + (state, action: PayloadAction) => { + state.updateContest.status = 'successful'; + state.updateContest.contest = action.payload; + }, + ); + builder.addCase(updateContest.rejected, (state, action: any) => { + state.updateContest.status = 'failed'; + state.updateContest.error = action.payload; + }); + + // 🆕 deleteContest + builder.addCase(deleteContest.pending, (state) => { + state.deleteContest.status = 'loading'; + state.deleteContest.error = undefined; + }); + builder.addCase( + deleteContest.fulfilled, + (state, action: PayloadAction) => { + state.deleteContest.status = 'successful'; + // Удалим контест из списков + state.fetchContests.contests = + state.fetchContests.contests.filter( + (c) => c.id !== action.payload, + ); + state.fetchMyContests.contests = + state.fetchMyContests.contests.filter( + (c) => c.id !== action.payload, + ); + }, + ); + builder.addCase(deleteContest.rejected, (state, action: any) => { + state.deleteContest.status = 'failed'; + state.deleteContest.error = action.payload; + }); + + // fetchMyContests + builder.addCase(fetchMyContests.pending, (state) => { + state.fetchMyContests.status = 'loading'; + state.fetchMyContests.error = undefined; + }); + builder.addCase( + fetchMyContests.fulfilled, + (state, action: PayloadAction) => { + state.fetchMyContests.status = 'successful'; + state.fetchMyContests.contests = action.payload; + }, + ); + builder.addCase(fetchMyContests.rejected, (state, action: any) => { + state.fetchMyContests.status = 'failed'; + state.fetchMyContests.error = action.payload; + }); + + // fetchRegisteredContests + builder.addCase(fetchRegisteredContests.pending, (state) => { + state.fetchRegisteredContests.status = 'loading'; + state.fetchRegisteredContests.error = undefined; + }); + builder.addCase( + fetchRegisteredContests.fulfilled, + (state, action: PayloadAction) => { + state.fetchRegisteredContests.status = 'successful'; + state.fetchRegisteredContests.contests = + action.payload.contests; + state.fetchRegisteredContests.hasNextPage = + action.payload.hasNextPage; }, ); builder.addCase( - createContest.rejected, - (state, action: PayloadAction) => { - state.statuses.create = 'failed'; - state.error = action.payload; + fetchRegisteredContests.rejected, + (state, action: any) => { + state.fetchRegisteredContests.status = 'failed'; + state.fetchRegisteredContests.error = action.payload; }, ); }, @@ -241,5 +570,5 @@ const contestsSlice = createSlice({ // Экспорты // ===================== -export const { clearSelectedContest, setContestStatus } = contestsSlice.actions; +export const { setContestStatus } = contestsSlice.actions; export const contestsReducer = contestsSlice.reducer; diff --git a/src/redux/slices/groupfeed.ts b/src/redux/slices/groupfeed.ts new file mode 100644 index 0000000..05cfdbb --- /dev/null +++ b/src/redux/slices/groupfeed.ts @@ -0,0 +1,347 @@ +import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; +import axios from '../../axios'; + +// ===================== +// Типы +// ===================== + +type Status = 'idle' | 'loading' | 'successful' | 'failed'; + +export interface Post { + id: number; + groupId: number; + authorId: number; + authorUsername: string; + name: string; + content: string; + createdAt: string; + updatedAt: string; +} + +export interface PostsPage { + items: Post[]; + hasNext: boolean; +} + +// ===================== +// Состояние +// ===================== + +interface PostsState { + fetchPosts: { + pages: Record; // страница => данные + status: Status; + error?: string; + }; + fetchPostById: { + post?: Post; + status: Status; + error?: string; + }; + createPost: { + post?: Post; + status: Status; + error?: string; + }; + updatePost: { + post?: Post; + status: Status; + error?: string; + }; + deletePost: { + deletedId?: number; + status: Status; + error?: string; + }; +} + +const initialState: PostsState = { + fetchPosts: { + pages: {}, + status: 'idle', + error: undefined, + }, + fetchPostById: { + post: undefined, + status: 'idle', + error: undefined, + }, + createPost: { + post: undefined, + status: 'idle', + error: undefined, + }, + updatePost: { + post: undefined, + status: 'idle', + error: undefined, + }, + deletePost: { + deletedId: undefined, + status: 'idle', + error: undefined, + }, +}; + +// ===================== +// Async Thunks +// ===================== + +// Получить посты группы (пагинация) +export const fetchGroupPosts = createAsyncThunk( + 'posts/fetchGroupPosts', + async ( + { + groupId, + page = 0, + pageSize = 20, + }: { groupId: number; page?: number; pageSize?: number }, + { rejectWithValue }, + ) => { + try { + const response = await axios.get( + `/groups/${groupId}/feed?page=${page}&pageSize=${pageSize}`, + ); + return { page, data: response.data as PostsPage }; + } catch (err: any) { + return rejectWithValue( + err.response?.data?.message || 'Ошибка загрузки постов', + ); + } + }, +); + +// Получить один пост +export const fetchPostById = createAsyncThunk( + 'posts/fetchPostById', + async ( + { groupId, postId }: { groupId: number; postId: number }, + { rejectWithValue }, + ) => { + try { + const response = await axios.get( + `/groups/${groupId}/feed/${postId}`, + ); + return response.data as Post; + } catch (err: any) { + return rejectWithValue( + err.response?.data?.message || 'Ошибка загрузки поста', + ); + } + }, +); + +// Создать пост +export const createPost = createAsyncThunk( + 'posts/createPost', + async ( + { + groupId, + name, + content, + }: { groupId: number; name: string; content: string }, + { rejectWithValue }, + ) => { + try { + const response = await axios.post(`/groups/${groupId}/feed`, { + name, + content, + }); + return response.data as Post; + } catch (err: any) { + return rejectWithValue( + err.response?.data?.message || 'Ошибка создания поста', + ); + } + }, +); + +// Обновить пост +export const updatePost = createAsyncThunk( + 'posts/updatePost', + async ( + { + groupId, + postId, + name, + content, + }: { + groupId: number; + postId: number; + name: string; + content: string; + }, + { rejectWithValue }, + ) => { + try { + const response = await axios.put( + `/groups/${groupId}/feed/${postId}`, + { + name, + content, + }, + ); + return response.data as Post; + } catch (err: any) { + return rejectWithValue( + err.response?.data?.message || 'Ошибка обновления поста', + ); + } + }, +); + +// Удалить пост +export const deletePost = createAsyncThunk( + 'posts/deletePost', + async ( + { groupId, postId }: { groupId: number; postId: number }, + { rejectWithValue }, + ) => { + try { + await axios.delete(`/groups/${groupId}/feed/${postId}`); + return postId; + } catch (err: any) { + return rejectWithValue( + err.response?.data?.message || 'Ошибка удаления поста', + ); + } + }, +); + +// ===================== +// Slice +// ===================== + +const postsSlice = createSlice({ + name: 'posts', + initialState, + reducers: { + setGroupFeedStatus: ( + state, + action: PayloadAction<{ key: keyof PostsState; status: Status }>, + ) => { + const { key, status } = action.payload; + if (state[key]) { + (state[key] as any).status = status; + } + }, + }, + extraReducers: (builder) => { + // fetchGroupPosts + builder.addCase(fetchGroupPosts.pending, (state) => { + state.fetchPosts.status = 'loading'; + }); + builder.addCase( + fetchGroupPosts.fulfilled, + ( + state, + action: PayloadAction<{ page: number; data: PostsPage }>, + ) => { + const { page, data } = action.payload; + state.fetchPosts.status = 'successful'; + state.fetchPosts.pages[page] = data; + }, + ); + builder.addCase(fetchGroupPosts.rejected, (state, action: any) => { + state.fetchPosts.status = 'failed'; + state.fetchPosts.error = action.payload; + }); + + // fetchPostById + builder.addCase(fetchPostById.pending, (state) => { + state.fetchPostById.status = 'loading'; + }); + builder.addCase( + fetchPostById.fulfilled, + (state, action: PayloadAction) => { + state.fetchPostById.status = 'successful'; + state.fetchPostById.post = action.payload; + }, + ); + builder.addCase(fetchPostById.rejected, (state, action: any) => { + state.fetchPostById.status = 'failed'; + state.fetchPostById.error = action.payload; + }); + + // createPost + builder.addCase(createPost.pending, (state) => { + state.createPost.status = 'loading'; + }); + builder.addCase( + createPost.fulfilled, + (state, action: PayloadAction) => { + state.createPost.status = 'successful'; + state.createPost.post = action.payload; + + // добавляем сразу в первую страницу (page = 0) + if (state.fetchPosts.pages[0]) { + state.fetchPosts.pages[0].items.unshift(action.payload); + } + }, + ); + builder.addCase(createPost.rejected, (state, action: any) => { + state.createPost.status = 'failed'; + state.createPost.error = action.payload; + }); + + // updatePost + builder.addCase(updatePost.pending, (state) => { + state.updatePost.status = 'loading'; + }); + builder.addCase( + updatePost.fulfilled, + (state, action: PayloadAction) => { + state.updatePost.status = 'successful'; + state.updatePost.post = action.payload; + + // обновим в списках + for (const page of Object.values(state.fetchPosts.pages)) { + const index = page.items.findIndex( + (p) => p.id === action.payload.id, + ); + if (index !== -1) page.items[index] = action.payload; + } + + // обновим если открыт одиночный пост + if (state.fetchPostById.post?.id === action.payload.id) { + state.fetchPostById.post = action.payload; + } + }, + ); + builder.addCase(updatePost.rejected, (state, action: any) => { + state.updatePost.status = 'failed'; + state.updatePost.error = action.payload; + }); + + // deletePost + builder.addCase(deletePost.pending, (state) => { + state.deletePost.status = 'loading'; + }); + builder.addCase( + deletePost.fulfilled, + (state, action: PayloadAction) => { + state.deletePost.status = 'successful'; + state.deletePost.deletedId = action.payload; + + // удалить из всех страниц + for (const page of Object.values(state.fetchPosts.pages)) { + page.items = page.items.filter( + (p) => p.id !== action.payload, + ); + } + + // если открыт индивидуальный пост + if (state.fetchPostById.post?.id === action.payload) { + state.fetchPostById.post = undefined; + } + }, + ); + builder.addCase(deletePost.rejected, (state, action: any) => { + state.deletePost.status = 'failed'; + state.deletePost.error = action.payload; + }); + }, +}); + +export const { setGroupFeedStatus } = postsSlice.actions; +export const groupFeedReducer = postsSlice.reducer; diff --git a/src/redux/slices/groups.ts b/src/redux/slices/groups.ts index e9c586d..38350bb 100644 --- a/src/redux/slices/groups.ts +++ b/src/redux/slices/groups.ts @@ -1,7 +1,9 @@ import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; import axios from '../../axios'; -// ─── Типы ──────────────────────────────────────────── +// ===================== +// Типы +// ===================== type Status = 'idle' | 'loading' | 'successful' | 'failed'; @@ -19,39 +21,106 @@ export interface Group { contests: any[]; } +// ===================== +// Состояние +// ===================== + interface GroupsState { - groups: Group[]; - currentGroup: Group | null; - statuses: { - create: Status; - update: Status; - delete: Status; - fetchMy: Status; - fetchById: Status; - addMember: Status; - removeMember: Status; + fetchMyGroups: { + groups: Group[]; + status: Status; + error?: string; + }; + fetchGroupById: { + group?: Group; + status: Status; + error?: string; + }; + createGroup: { + group?: Group; + status: Status; + error?: string; + }; + updateGroup: { + group?: Group; + status: Status; + error?: string; + }; + deleteGroup: { + deletedId?: number; + status: Status; + error?: string; + }; + addGroupMember: { + status: Status; + error?: string; + }; + removeGroupMember: { + status: Status; + error?: string; + }; + fetchGroupJoinLink: { + joinLink?: { token: string; expiresAt: string }; + status: Status; + error?: string; + }; + joinGroupByToken: { + group?: Group; + status: Status; + error?: string; }; - error: string | null; } const initialState: GroupsState = { - groups: [], - currentGroup: null, - statuses: { - create: 'idle', - update: 'idle', - delete: 'idle', - fetchMy: 'idle', - fetchById: 'idle', - addMember: 'idle', - removeMember: 'idle', + fetchMyGroups: { + groups: [], + status: 'idle', + error: undefined, + }, + fetchGroupById: { + group: undefined, + status: 'idle', + error: undefined, + }, + createGroup: { + group: undefined, + status: 'idle', + error: undefined, + }, + updateGroup: { + group: undefined, + status: 'idle', + error: undefined, + }, + deleteGroup: { + deletedId: undefined, + status: 'idle', + error: undefined, + }, + addGroupMember: { + status: 'idle', + error: undefined, + }, + removeGroupMember: { + status: 'idle', + error: undefined, + }, + fetchGroupJoinLink: { + joinLink: undefined, + status: 'idle', + error: undefined, + }, + joinGroupByToken: { + group: undefined, + status: 'idle', + error: undefined, }, - error: null, }; -// ─── Async Thunks ───────────────────────────────────── +// ===================== +// Async Thunks +// ===================== -// POST /groups export const createGroup = createAsyncThunk( 'groups/createGroup', async ( @@ -69,7 +138,6 @@ export const createGroup = createAsyncThunk( }, ); -// PUT /groups/{groupId} export const updateGroup = createAsyncThunk( 'groups/updateGroup', async ( @@ -94,7 +162,6 @@ export const updateGroup = createAsyncThunk( }, ); -// DELETE /groups/{groupId} export const deleteGroup = createAsyncThunk( 'groups/deleteGroup', async (groupId: number, { rejectWithValue }) => { @@ -109,7 +176,6 @@ export const deleteGroup = createAsyncThunk( }, ); -// GET /groups/my export const fetchMyGroups = createAsyncThunk( 'groups/fetchMyGroups', async (_, { rejectWithValue }) => { @@ -124,7 +190,6 @@ export const fetchMyGroups = createAsyncThunk( }, ); -// GET /groups/{groupId} export const fetchGroupById = createAsyncThunk( 'groups/fetchGroupById', async (groupId: number, { rejectWithValue }) => { @@ -139,16 +204,22 @@ export const fetchGroupById = createAsyncThunk( }, ); -// POST /groups/members export const addGroupMember = createAsyncThunk( 'groups/addGroupMember', async ( - { userId, role }: { userId: number; role: string }, + { + groupId, + userId, + role, + }: { groupId: number; userId: number; role: string }, { rejectWithValue }, ) => { try { - await axios.post('/groups/members', { userId, role }); - return { userId, role }; + const response = await axios.post(`/groups/${groupId}/members`, { + userId, + role, + }); + return response.data; } catch (err: any) { return rejectWithValue( err.response?.data?.message || @@ -158,7 +229,6 @@ export const addGroupMember = createAsyncThunk( }, ); -// DELETE /groups/{groupId}/members/{memberId} export const removeGroupMember = createAsyncThunk( 'groups/removeGroupMember', async ( @@ -176,147 +246,169 @@ export const removeGroupMember = createAsyncThunk( }, ); -// ─── Slice ──────────────────────────────────────────── +// ===================== +// Новые Async Thunks +// ===================== + +// Получение актуальной ссылки для присоединения к группе +export const fetchGroupJoinLink = createAsyncThunk( + 'groups/fetchGroupJoinLink', + async (groupId: number, { rejectWithValue }) => { + try { + const response = await axios.get(`/groups/${groupId}/join-link`); + return response.data as { token: string; expiresAt: string }; + } catch (err: any) { + return rejectWithValue( + err.response?.data?.message || + 'Ошибка при получении ссылки для присоединения', + ); + } + }, +); + +// Присоединение к группе по токену приглашения +export const joinGroupByToken = createAsyncThunk( + 'groups/joinGroupByToken', + async (token: string, { rejectWithValue }) => { + try { + const response = await axios.post(`/groups/join/${token}`); + return response.data as Group; + } catch (err: any) { + return rejectWithValue( + err.response?.data?.message || + 'Ошибка при присоединении к группе по ссылке', + ); + } + }, +); + +// ===================== +// Slice +// ===================== const groupsSlice = createSlice({ name: 'groups', initialState, reducers: { - clearCurrentGroup: (state) => { - state.currentGroup = null; + setGroupsStatus: ( + state, + action: PayloadAction<{ key: keyof GroupsState; status: Status }>, + ) => { + const { key, status } = action.payload; + if (state[key]) { + (state[key] as any).status = status; + } }, }, extraReducers: (builder) => { - // ─── CREATE GROUP ─── - builder.addCase(createGroup.pending, (state) => { - state.statuses.create = 'loading'; - state.error = null; - }); - builder.addCase( - createGroup.fulfilled, - (state, action: PayloadAction) => { - state.statuses.create = 'successful'; - state.groups.push(action.payload); - }, - ); - builder.addCase( - createGroup.rejected, - (state, action: PayloadAction) => { - state.statuses.create = 'failed'; - state.error = action.payload; - }, - ); - - // ─── UPDATE GROUP ─── - builder.addCase(updateGroup.pending, (state) => { - state.statuses.update = 'loading'; - state.error = null; - }); - builder.addCase( - updateGroup.fulfilled, - (state, action: PayloadAction) => { - state.statuses.update = 'successful'; - const index = state.groups.findIndex( - (g) => g.id === action.payload.id, - ); - if (index !== -1) state.groups[index] = action.payload; - if (state.currentGroup?.id === action.payload.id) { - state.currentGroup = action.payload; - } - }, - ); - builder.addCase( - updateGroup.rejected, - (state, action: PayloadAction) => { - state.statuses.update = 'failed'; - state.error = action.payload; - }, - ); - - // ─── DELETE GROUP ─── - builder.addCase(deleteGroup.pending, (state) => { - state.statuses.delete = 'loading'; - state.error = null; - }); - builder.addCase( - deleteGroup.fulfilled, - (state, action: PayloadAction) => { - state.statuses.delete = 'successful'; - state.groups = state.groups.filter( - (g) => g.id !== action.payload, - ); - if (state.currentGroup?.id === action.payload) - state.currentGroup = null; - }, - ); - builder.addCase( - deleteGroup.rejected, - (state, action: PayloadAction) => { - state.statuses.delete = 'failed'; - state.error = action.payload; - }, - ); - - // ─── FETCH MY GROUPS ─── + // fetchMyGroups builder.addCase(fetchMyGroups.pending, (state) => { - state.statuses.fetchMy = 'loading'; - state.error = null; + state.fetchMyGroups.status = 'loading'; }); builder.addCase( fetchMyGroups.fulfilled, (state, action: PayloadAction) => { - state.statuses.fetchMy = 'successful'; - state.groups = action.payload; - }, - ); - builder.addCase( - fetchMyGroups.rejected, - (state, action: PayloadAction) => { - state.statuses.fetchMy = 'failed'; - state.error = action.payload; + state.fetchMyGroups.status = 'successful'; + state.fetchMyGroups.groups = action.payload; }, ); + builder.addCase(fetchMyGroups.rejected, (state, action: any) => { + state.fetchMyGroups.status = 'failed'; + state.fetchMyGroups.error = action.payload; + }); - // ─── FETCH GROUP BY ID ─── + // fetchGroupById builder.addCase(fetchGroupById.pending, (state) => { - state.statuses.fetchById = 'loading'; - state.error = null; + state.fetchGroupById.status = 'loading'; }); builder.addCase( fetchGroupById.fulfilled, (state, action: PayloadAction) => { - state.statuses.fetchById = 'successful'; - state.currentGroup = action.payload; - }, - ); - builder.addCase( - fetchGroupById.rejected, - (state, action: PayloadAction) => { - state.statuses.fetchById = 'failed'; - state.error = action.payload; + state.fetchGroupById.status = 'successful'; + state.fetchGroupById.group = action.payload; }, ); + builder.addCase(fetchGroupById.rejected, (state, action: any) => { + state.fetchGroupById.status = 'failed'; + state.fetchGroupById.error = action.payload; + }); - // ─── ADD MEMBER ─── + // createGroup + builder.addCase(createGroup.pending, (state) => { + state.createGroup.status = 'loading'; + }); + builder.addCase( + createGroup.fulfilled, + (state, action: PayloadAction) => { + state.createGroup.status = 'successful'; + state.createGroup.group = action.payload; + state.fetchMyGroups.groups.push(action.payload); + }, + ); + builder.addCase(createGroup.rejected, (state, action: any) => { + state.createGroup.status = 'failed'; + state.createGroup.error = action.payload; + }); + + // updateGroup + builder.addCase(updateGroup.pending, (state) => { + state.updateGroup.status = 'loading'; + }); + builder.addCase( + updateGroup.fulfilled, + (state, action: PayloadAction) => { + state.updateGroup.status = 'successful'; + state.updateGroup.group = action.payload; + const index = state.fetchMyGroups.groups.findIndex( + (g) => g.id === action.payload.id, + ); + if (index !== -1) + state.fetchMyGroups.groups[index] = action.payload; + if (state.fetchGroupById.group?.id === action.payload.id) + state.fetchGroupById.group = action.payload; + }, + ); + builder.addCase(updateGroup.rejected, (state, action: any) => { + state.updateGroup.status = 'failed'; + state.updateGroup.error = action.payload; + }); + + // deleteGroup + builder.addCase(deleteGroup.pending, (state) => { + state.deleteGroup.status = 'loading'; + }); + builder.addCase( + deleteGroup.fulfilled, + (state, action: PayloadAction) => { + state.deleteGroup.status = 'successful'; + state.deleteGroup.deletedId = action.payload; + state.fetchMyGroups.groups = state.fetchMyGroups.groups.filter( + (g) => g.id !== action.payload, + ); + if (state.fetchGroupById.group?.id === action.payload) + state.fetchGroupById.group = undefined; + }, + ); + builder.addCase(deleteGroup.rejected, (state, action: any) => { + state.deleteGroup.status = 'failed'; + state.deleteGroup.error = action.payload; + }); + + // addGroupMember builder.addCase(addGroupMember.pending, (state) => { - state.statuses.addMember = 'loading'; - state.error = null; + state.addGroupMember.status = 'loading'; }); builder.addCase(addGroupMember.fulfilled, (state) => { - state.statuses.addMember = 'successful'; + state.addGroupMember.status = 'successful'; + }); + builder.addCase(addGroupMember.rejected, (state, action: any) => { + state.addGroupMember.status = 'failed'; + state.addGroupMember.error = action.payload; }); - builder.addCase( - addGroupMember.rejected, - (state, action: PayloadAction) => { - state.statuses.addMember = 'failed'; - state.error = action.payload; - }, - ); - // ─── REMOVE MEMBER ─── + // removeGroupMember builder.addCase(removeGroupMember.pending, (state) => { - state.statuses.removeMember = 'loading'; - state.error = null; + state.removeGroupMember.status = 'loading'; }); builder.addCase( removeGroupMember.fulfilled, @@ -324,27 +416,60 @@ const groupsSlice = createSlice({ state, action: PayloadAction<{ groupId: number; memberId: number }>, ) => { - state.statuses.removeMember = 'successful'; + state.removeGroupMember.status = 'successful'; if ( - state.currentGroup && - state.currentGroup.id === action.payload.groupId + state.fetchGroupById.group && + state.fetchGroupById.group.id === action.payload.groupId ) { - state.currentGroup.members = - state.currentGroup.members.filter( + state.fetchGroupById.group.members = + state.fetchGroupById.group.members.filter( (m) => m.userId !== action.payload.memberId, ); } }, ); + builder.addCase(removeGroupMember.rejected, (state, action: any) => { + state.removeGroupMember.status = 'failed'; + state.removeGroupMember.error = action.payload; + }); + + // fetchGroupJoinLink + builder.addCase(fetchGroupJoinLink.pending, (state) => { + state.fetchGroupJoinLink.status = 'loading'; + }); builder.addCase( - removeGroupMember.rejected, - (state, action: PayloadAction) => { - state.statuses.removeMember = 'failed'; - state.error = action.payload; + fetchGroupJoinLink.fulfilled, + ( + state, + action: PayloadAction<{ token: string; expiresAt: string }>, + ) => { + state.fetchGroupJoinLink.status = 'successful'; + state.fetchGroupJoinLink.joinLink = action.payload; }, ); + builder.addCase(fetchGroupJoinLink.rejected, (state, action: any) => { + state.fetchGroupJoinLink.status = 'failed'; + state.fetchGroupJoinLink.error = action.payload; + }); + + // joinGroupByToken + builder.addCase(joinGroupByToken.pending, (state) => { + state.joinGroupByToken.status = 'loading'; + }); + builder.addCase( + joinGroupByToken.fulfilled, + (state, action: PayloadAction) => { + state.joinGroupByToken.status = 'successful'; + state.joinGroupByToken.group = action.payload; + state.fetchMyGroups.groups.push(action.payload); // добавим новую группу в список + }, + ); + builder.addCase(joinGroupByToken.rejected, (state, action: any) => { + state.joinGroupByToken.status = 'failed'; + state.joinGroupByToken.error = action.payload; + }); }, }); -export const { clearCurrentGroup } = groupsSlice.actions; +export const { setGroupsStatus } = groupsSlice.actions; export const groupsReducer = groupsSlice.reducer; diff --git a/src/redux/slices/missions.ts b/src/redux/slices/missions.ts index 93f24eb..f3c84ee 100644 --- a/src/redux/slices/missions.ts +++ b/src/redux/slices/missions.ts @@ -20,6 +20,8 @@ export interface Mission { tags: string[]; createdAt: string; updatedAt: string; + timeLimit: number; + memoryLimit: number; statements?: Statement[]; } @@ -31,6 +33,7 @@ interface MissionsState { fetchList: Status; fetchById: Status; upload: Status; + fetchMy: Status; }; error: string | null; } @@ -45,6 +48,7 @@ const initialState: MissionsState = { fetchList: 'idle', fetchById: 'idle', upload: 'idle', + fetchMy: 'idle', }, error: null, }; @@ -90,6 +94,22 @@ export const fetchMissionById = createAsyncThunk( }, ); +// ✅ GET /missions/my +export const fetchMyMissions = createAsyncThunk( + 'missions/fetchMyMissions', + async (_, { rejectWithValue }) => { + try { + const response = await axios.get('/missions/my'); + return response.data as Mission[]; // массив миссий пользователя + } catch (err: any) { + return rejectWithValue( + err.response?.data?.message || + 'Ошибка при получении моих миссий', + ); + } + }, +); + // POST /missions/upload export const uploadMission = createAsyncThunk( 'missions/uploadMission', @@ -127,9 +147,6 @@ const missionsSlice = createSlice({ name: 'missions', initialState, reducers: { - clearCurrentMission: (state) => { - state.currentMission = null; - }, setMissionsStatus: ( state, action: PayloadAction<{ @@ -189,6 +206,26 @@ const missionsSlice = createSlice({ }, ); + // ✅ FETCH MY MISSIONS ─── + builder.addCase(fetchMyMissions.pending, (state) => { + state.statuses.fetchMy = 'loading'; + state.error = null; + }); + builder.addCase( + fetchMyMissions.fulfilled, + (state, action: PayloadAction) => { + state.statuses.fetchMy = 'successful'; + state.missions = action.payload; + }, + ); + builder.addCase( + fetchMyMissions.rejected, + (state, action: PayloadAction) => { + state.statuses.fetchMy = 'failed'; + state.error = action.payload; + }, + ); + // ─── UPLOAD MISSION ─── builder.addCase(uploadMission.pending, (state) => { state.statuses.upload = 'loading'; @@ -211,5 +248,5 @@ const missionsSlice = createSlice({ }, }); -export const { clearCurrentMission, setMissionsStatus } = missionsSlice.actions; +export const { setMissionsStatus } = missionsSlice.actions; export const missionsReducer = missionsSlice.reducer; diff --git a/src/redux/slices/store.ts b/src/redux/slices/store.ts index bd54ba6..cc713a0 100644 --- a/src/redux/slices/store.ts +++ b/src/redux/slices/store.ts @@ -5,6 +5,7 @@ interface StorState { menu: { activePage: string; activeProfilePage: string; + activeGroupPage: string; }; } @@ -13,6 +14,7 @@ const initialState: StorState = { menu: { activePage: '', activeProfilePage: '', + activeGroupPage: '', }, }; @@ -30,9 +32,19 @@ const storeSlice = createSlice({ ) => { state.menu.activeProfilePage = activeProfilePage.payload; }, + setMenuActiveGroupPage: ( + state, + activeGroupPage: PayloadAction, + ) => { + state.menu.activeGroupPage = activeGroupPage.payload; + }, }, }); -export const { setMenuActivePage, setMenuActiveProfilePage } = - storeSlice.actions; +export const { + setMenuActivePage, + setMenuActiveProfilePage, + setMenuActiveGroupPage, +} = storeSlice.actions; + export const storeReducer = storeSlice.reducer; diff --git a/src/redux/slices/submit.ts b/src/redux/slices/submit.ts index b70efe8..21522b1 100644 --- a/src/redux/slices/submit.ts +++ b/src/redux/slices/submit.ts @@ -8,7 +8,7 @@ export interface Submit { language: string; languageVersion: string; sourceCode: string; - contestId: number | null; + contestId?: number; } export interface Solution { @@ -30,8 +30,8 @@ export interface MissionSubmit { id: number; userId: number; solution: Solution; - contestId: number | null; - contestName: string | null; + contestId?: number; + contestName?: string; sourceType: string; } @@ -40,7 +40,7 @@ interface SubmitState { submitsById: Record; // ✅ добавлено currentSubmit?: Submit; status: 'idle' | 'loading' | 'successful' | 'failed'; - error: string | null; + error?: string; } // Начальное состояние @@ -49,7 +49,7 @@ const initialState: SubmitState = { submitsById: {}, // ✅ инициализация currentSubmit: undefined, status: 'idle', - error: null, + error: undefined, }; // AsyncThunk: Отправка решения @@ -123,7 +123,7 @@ const submitSlice = createSlice({ clearCurrentSubmit: (state) => { state.currentSubmit = undefined; state.status = 'idle'; - state.error = null; + state.error = undefined; }, clearSubmitsByMission: (state, action: PayloadAction) => { delete state.submitsById[action.payload]; @@ -133,7 +133,7 @@ const submitSlice = createSlice({ // Отправка решения builder.addCase(submitMission.pending, (state) => { state.status = 'loading'; - state.error = null; + state.error = undefined; }); builder.addCase( submitMission.fulfilled, @@ -153,7 +153,7 @@ const submitSlice = createSlice({ // Получить все свои отправки builder.addCase(fetchMySubmits.pending, (state) => { state.status = 'loading'; - state.error = null; + state.error = undefined; }); builder.addCase( fetchMySubmits.fulfilled, @@ -173,7 +173,7 @@ const submitSlice = createSlice({ // Получить отправку по ID builder.addCase(fetchSubmitById.pending, (state) => { state.status = 'loading'; - state.error = null; + state.error = undefined; }); builder.addCase( fetchSubmitById.fulfilled, @@ -193,7 +193,7 @@ const submitSlice = createSlice({ // ✅ Получить отправки по миссии builder.addCase(fetchMySubmitsByMission.pending, (state) => { state.status = 'loading'; - state.error = null; + state.error = undefined; }); builder.addCase( fetchMySubmitsByMission.fulfilled, diff --git a/src/redux/store.ts b/src/redux/store.ts index edbe49c..84a6fb3 100644 --- a/src/redux/store.ts +++ b/src/redux/store.ts @@ -6,6 +6,7 @@ import { submitReducer } from './slices/submit'; import { contestsReducer } from './slices/contests'; import { groupsReducer } from './slices/groups'; import { articlesReducer } from './slices/articles'; +import { groupFeedReducer } from './slices/groupfeed'; // использование // import { useAppDispatch, useAppSelector } from '../redux/hooks'; @@ -25,6 +26,7 @@ export const store = configureStore({ contests: contestsReducer, groups: groupsReducer, articles: articlesReducer, + groupfeed: groupFeedReducer, }, }); diff --git a/src/styles/index.css b/src/styles/index.css index 69b3072..2896437 100644 --- a/src/styles/index.css +++ b/src/styles/index.css @@ -3,6 +3,7 @@ @import 'tailwindcss/utilities'; @import './latex-container.css'; +@import './toast.css'; * { -webkit-tap-highlight-color: transparent; /* Отключаем выделение синим при тапе на телефоне*/ diff --git a/src/styles/toast.css b/src/styles/toast.css new file mode 100644 index 0000000..c7f9b2c --- /dev/null +++ b/src/styles/toast.css @@ -0,0 +1,32 @@ +.Toastify__progress-bar--success { + background: #10be59 !important; +} + +.Toastify__toast--success .Toastify__toast-icon svg path { + fill: #10be59 !important; +} + +.Toastify__progress-bar--error { + background: #f13e5f !important; +} + +.Toastify__toast--error .Toastify__toast-icon svg path { + fill: #f13e5f !important; +} + +.Toastify__progress-bar--success { + background: #10be59 !important; +} + +.Toastify__toast--success .Toastify__toast-icon svg path { + fill: #10be59 !important; +} + +.Toastify__toast { + background: #292929 !important; + color: var(--color-liquid-white); +} + +.Toastify__toast > button > svg { + fill: var(--color-liquid-white); +} diff --git a/src/views/articleeditor/Editor.tsx b/src/views/articleeditor/Editor.tsx index 38e4e43..8fd18ad 100644 --- a/src/views/articleeditor/Editor.tsx +++ b/src/views/articleeditor/Editor.tsx @@ -4,18 +4,7 @@ import 'highlight.js/styles/github-dark.css'; import MarkdownPreview from './MarckDownPreview'; -interface MarkdownEditorProps { - defaultValue?: string; - onChange: (value: string) => void; -} - -const MarkdownEditor: FC = ({ - defaultValue, - onChange, -}) => { - const [markdown, setMarkdown] = useState( - defaultValue || - `# 🌙 Добро пожаловать в Markdown-редактор +export const MarkDownPattern = `# 🌙 Добро пожаловать в Markdown-редактор Добро пожаловать в **Markdown-редактор**! Здесь ты можешь писать в формате Markdown и видеть результат **в реальном времени** 👇 @@ -209,13 +198,29 @@ print(greet("Мир")) **🖤 Конец демонстрации. Спасибо, что используешь Markdown-редактор!** -`, +`; + +interface MarkdownEditorProps { + defaultValue?: string; + onChange: (value: string) => void; +} + +const MarkdownEditor: FC = ({ + defaultValue, + onChange, +}) => { + const [markdown, setMarkdown] = useState( + defaultValue || MarkDownPattern, ); useEffect(() => { onChange(markdown); }, [markdown]); + useEffect(() => { + setMarkdown(defaultValue || MarkDownPattern); + }, [defaultValue]); + // Обработчик вставки const handlePaste = async ( e: React.ClipboardEvent, diff --git a/src/views/articleeditor/MarckDownPreview.tsx b/src/views/articleeditor/MarckDownPreview.tsx index 36f5e38..3cfa04a 100644 --- a/src/views/articleeditor/MarckDownPreview.tsx +++ b/src/views/articleeditor/MarckDownPreview.tsx @@ -30,12 +30,7 @@ const MarkdownPreview: FC = ({ className = '', }) => { return ( -
+
{
- } - /> + } /> } /> - } - /> + } /> { (state) => state.store.menu.activeProfilePage, ); - console.log('active', [activeProfilePage]); - return (
{menuItems.map((v, i) => ( diff --git a/src/views/home/account/ContestsBlock.tsx b/src/views/home/account/ContestsBlock.tsx deleted file mode 100644 index 91b13ac..0000000 --- a/src/views/home/account/ContestsBlock.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { useEffect } from 'react'; -import { useAppDispatch } from '../../../redux/hooks'; -import { setMenuActiveProfilePage } from '../../../redux/slices/store'; - -const ContestsBlock = () => { - const dispatch = useAppDispatch(); - - useEffect(() => { - dispatch(setMenuActiveProfilePage('contests')); - }, []); - return ( -
- Пока пусто :( -
- ); -}; - -export default ContestsBlock; diff --git a/src/views/home/account/MissionsBlock.tsx b/src/views/home/account/MissionsBlock.tsx deleted file mode 100644 index 1fce2a8..0000000 --- a/src/views/home/account/MissionsBlock.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { useEffect } from 'react'; -import { useAppDispatch } from '../../../redux/hooks'; -import { setMenuActiveProfilePage } from '../../../redux/slices/store'; - -const MissionsBlock = () => { - const dispatch = useAppDispatch(); - - useEffect(() => { - dispatch(setMenuActiveProfilePage('missions')); - }, []); - - return ( -
- Пока пусто :( -
- ); -}; - -export default MissionsBlock; diff --git a/src/views/home/account/ArticlesBlock.tsx b/src/views/home/account/articles/ArticlesBlock.tsx similarity index 51% rename from src/views/home/account/ArticlesBlock.tsx rename to src/views/home/account/articles/ArticlesBlock.tsx index 1d4f141..eb91f9b 100644 --- a/src/views/home/account/ArticlesBlock.tsx +++ b/src/views/home/account/articles/ArticlesBlock.tsx @@ -1,10 +1,9 @@ import { FC, useEffect, useState } from 'react'; -import { useAppDispatch, useAppSelector } from '../../../redux/hooks'; -import { setMenuActiveProfilePage } from '../../../redux/slices/store'; -import { cn } from '../../../lib/cn'; -import { ChevroneDown, Edit } from '../../../assets/icons/groups'; -import { fetchArticles } from '../../../redux/slices/articles'; - +import { useAppDispatch, useAppSelector } from '../../../../redux/hooks'; +import { setMenuActiveProfilePage } from '../../../../redux/slices/store'; +import { cn } from '../../../../lib/cn'; +import { ChevroneDown, Edit } from '../../../../assets/icons/groups'; +import { fetchMyArticles } from '../../../../redux/slices/articles'; import { useNavigate } from 'react-router-dom'; export interface ArticleItemProps { @@ -13,21 +12,21 @@ export interface ArticleItemProps { tags: string[]; } -const ArticleItem: React.FC = ({ id, name, tags }) => { +const ArticleItem: FC = ({ id, name, tags }) => { const navigate = useNavigate(); + return (
{ - navigate(`/article/${id}?back=/home/account/articles`); - }} + onClick={() => + navigate(`/article/${id}?back=/home/account/articles`) + } > -
+
#{id}
@@ -35,13 +34,14 @@ const ArticleItem: React.FC = ({ id, name, tags }) => { {name}
+
{tags.map((v, i) => (
{v} @@ -50,8 +50,9 @@ const ArticleItem: React.FC = ({ id, name, tags }) => {
Редактировать { e.stopPropagation(); navigate( @@ -69,49 +70,79 @@ interface ArticlesBlockProps { const ArticlesBlock: FC = ({ className = '' }) => { const dispatch = useAppDispatch(); - const articles = useAppSelector((state) => state.articles.articles); const [active, setActive] = useState(true); + // ✅ Берём только "мои статьи" + const articles = useAppSelector( + (state) => state.articles.fetchMyArticles.articles, + ); + const status = useAppSelector( + (state) => state.articles.fetchMyArticles.status, + ); + const error = useAppSelector( + (state) => state.articles.fetchMyArticles.error, + ); + useEffect(() => { dispatch(setMenuActiveProfilePage('articles')); - dispatch(fetchArticles({})); - }, []); + dispatch(fetchMyArticles()); + }, [dispatch]); + return (
+ {/* Заголовок */}
{ - setActive(!active); - }} + onClick={() => setActive(!active)} > Мои статьи toggle
+ + {/* Контент */}
- {articles.map((v, i) => ( - + {status === 'loading' && ( +
+ Загрузка статей... +
+ )} + {status === 'failed' && ( +
+ Ошибка:{' '} + {error || 'Не удалось загрузить статьи'} +
+ )} + {status === 'successful' && + articles.length === 0 && ( +
+ У вас пока нет статей +
+ )} + {articles.map((v) => ( + ))}
diff --git a/src/views/home/account/contests/Contests.tsx b/src/views/home/account/contests/Contests.tsx new file mode 100644 index 0000000..0c62c1a --- /dev/null +++ b/src/views/home/account/contests/Contests.tsx @@ -0,0 +1,61 @@ +import { useEffect } from 'react'; +import { useAppDispatch, useAppSelector } from '../../../../redux/hooks'; +import { setMenuActiveProfilePage } from '../../../../redux/slices/store'; +import { + fetchMyContests, + fetchRegisteredContests, +} from '../../../../redux/slices/contests'; +import ContestsBlock from './ContestsBlock'; + +const Contests = () => { + const dispatch = useAppDispatch(); + + // Redux-состояния + const myContestsState = useAppSelector( + (state) => state.contests.fetchMyContests, + ); + + // При загрузке страницы — выставляем вкладку и подгружаем контесты + useEffect(() => { + dispatch(setMenuActiveProfilePage('contests')); + dispatch(fetchMyContests()); + dispatch(fetchRegisteredContests({})); + }, []); + + return ( +
+ {/* Контесты, в которых я участвую */} +
+ +
+ + {/* Контесты, которые я создал */} +
+ {myContestsState.status === 'loading' ? ( +
+ Загрузка ваших контестов... +
+ ) : myContestsState.error ? ( +
+ Ошибка: {myContestsState.error} +
+ ) : ( + + )} +
+
+ ); +}; + +export default Contests; diff --git a/src/views/home/account/contests/ContestsBlock.tsx b/src/views/home/account/contests/ContestsBlock.tsx new file mode 100644 index 0000000..f732ffe --- /dev/null +++ b/src/views/home/account/contests/ContestsBlock.tsx @@ -0,0 +1,93 @@ +import { useState, FC } from 'react'; +import { cn } from '../../../../lib/cn'; +import { ChevroneDown } from '../../../../assets/icons/groups'; +import MyContestItem from './MyContestItem'; +import RegisterContestItem from './RegisterContestItem'; +import { Contest } from '../../../../redux/slices/contests'; + +interface ContestsBlockProps { + contests: Contest[]; + title: string; + className?: string; + type?: 'my' | 'reg'; +} + +const ContestsBlock: FC = ({ + contests, + title, + className, + type = 'my', +}) => { + const [active, setActive] = useState(title != 'Скрытые'); + + return ( +
+
{ + setActive(!active); + }} + > + {title} + +
+
+
+
+ {contests.map((v, i) => { + return type == 'my' ? ( + + ) : ( + + ); + })} +
+
+
+
+ ); +}; + +export default ContestsBlock; diff --git a/src/views/home/account/contests/MyContestItem.tsx b/src/views/home/account/contests/MyContestItem.tsx new file mode 100644 index 0000000..eef1bf4 --- /dev/null +++ b/src/views/home/account/contests/MyContestItem.tsx @@ -0,0 +1,98 @@ +import { cn } from '../../../../lib/cn'; +import { Account } from '../../../../assets/icons/auth'; +import { useNavigate } from 'react-router-dom'; +import { Edit } from '../../../../assets/icons/input'; + +export interface ContestItemProps { + id: number; + name: string; + startAt: string; + duration: number; + members: number; + type: 'first' | 'second'; +} + +function formatDate(dateString: string): string { + const date = new Date(dateString); + + const day = date.getDate().toString().padStart(2, '0'); + const month = (date.getMonth() + 1).toString().padStart(2, '0'); + const year = date.getFullYear(); + + const hours = date.getHours().toString().padStart(2, '0'); + const minutes = date.getMinutes().toString().padStart(2, '0'); + + return `${day}/${month}/${year}\n${hours}:${minutes}`; +} + +function formatWaitTime(ms: number): string { + const minutes = Math.floor(ms / 60000); + const hours = Math.floor(minutes / 60); + const days = Math.floor(hours / 24); + + if (days > 0) { + const remainder = days % 10; + let suffix = 'дней'; + if (remainder === 1 && days !== 11) suffix = 'день'; + else if (remainder >= 2 && remainder <= 4 && (days < 10 || days > 20)) + suffix = 'дня'; + return `${days} ${suffix}`; + } else if (hours > 0) { + const mins = minutes % 60; + return mins > 0 ? `${hours} ч ${mins} мин` : `${hours} ч`; + } else { + return `${minutes} мин`; + } +} + +const ContestItem: React.FC = ({ + id, + name, + startAt, + duration, + members, + type, +}) => { + const navigate = useNavigate(); + + return ( +
{ + navigate(`/contest/${id}`); + }} + > +
{name}
+
+ {/* {authors.map((v, i) =>

{v}

)} */} + valavshonok +
+
+ {formatDate(startAt)} +
+
{formatWaitTime(duration)}
+
+
{members}
+ +
+ + { + e.stopPropagation(); + navigate( + `/contest/create?back=/home/account/contests&contestId=${id}`, + ); + }} + /> +
+ ); +}; + +export default ContestItem; diff --git a/src/views/home/account/contests/RegisterContestItem.tsx b/src/views/home/account/contests/RegisterContestItem.tsx new file mode 100644 index 0000000..f8cbf6c --- /dev/null +++ b/src/views/home/account/contests/RegisterContestItem.tsx @@ -0,0 +1,114 @@ +import { cn } from '../../../../lib/cn'; +import { Account } from '../../../../assets/icons/auth'; +import { PrimaryButton } from '../../../../components/button/PrimaryButton'; +import { ReverseButton } from '../../../../components/button/ReverseButton'; +import { useNavigate } from 'react-router-dom'; + +export interface ContestItemProps { + id: number; + name: string; + startAt: string; + duration: number; + members: number; + statusRegister: 'reg' | 'nonreg'; + type: 'first' | 'second'; +} + +function formatDate(dateString: string): string { + const date = new Date(dateString); + + const day = date.getDate().toString().padStart(2, '0'); + const month = (date.getMonth() + 1).toString().padStart(2, '0'); + const year = date.getFullYear(); + + const hours = date.getHours().toString().padStart(2, '0'); + const minutes = date.getMinutes().toString().padStart(2, '0'); + + return `${day}/${month}/${year}\n${hours}:${minutes}`; +} + +function formatWaitTime(ms: number): string { + const minutes = Math.floor(ms / 60000); + const hours = Math.floor(minutes / 60); + const days = Math.floor(hours / 24); + + if (days > 0) { + const remainder = days % 10; + let suffix = 'дней'; + if (remainder === 1 && days !== 11) suffix = 'день'; + else if (remainder >= 2 && remainder <= 4 && (days < 10 || days > 20)) + suffix = 'дня'; + return `${days} ${suffix}`; + } else if (hours > 0) { + const mins = minutes % 60; + return mins > 0 ? `${hours} ч ${mins} мин` : `${hours} ч`; + } else { + return `${minutes} мин`; + } +} + +const ContestItem: React.FC = ({ + id, + name, + startAt, + duration, + members, + statusRegister, + type, +}) => { + const navigate = useNavigate(); + + const now = new Date(); + + const waitTime = new Date(startAt).getTime() - now.getTime(); + + return ( +
{ + navigate(`/contest/${id}`); + }} + > +
{name}
+
+ {/* {authors.map((v, i) =>

{v}

)} */} + valavshonok +
+
+ {formatDate(startAt)} +
+
{formatWaitTime(duration)}
+ {waitTime > 0 && ( +
+ {'До начала\n' + formatWaitTime(waitTime)} +
+ )} +
+
{members}
+ +
+
+ {statusRegister == 'reg' ? ( + <> + {' '} + {}} text="Регистрация" /> + + ) : ( + <> + {' '} + {}} text="Вы записаны" /> + + )} +
+
+ ); +}; + +export default ContestItem; diff --git a/src/views/home/account/missions/Missions.tsx b/src/views/home/account/missions/Missions.tsx new file mode 100644 index 0000000..1d8d2fb --- /dev/null +++ b/src/views/home/account/missions/Missions.tsx @@ -0,0 +1,109 @@ +import { FC, useEffect } from 'react'; +import { useAppDispatch, useAppSelector } from '../../../../redux/hooks'; +import { setMenuActiveProfilePage } from '../../../../redux/slices/store'; +import { cn } from '../../../../lib/cn'; +import MissionsBlock from './MissionsBlock'; +import { + fetchMyMissions, + setMissionsStatus, +} from '../../../../redux/slices/missions'; + +interface ItemProps { + count: number; + totalCount: number; + title: string; + color?: 'default' | 'red' | 'green' | 'orange'; +} + +const Item: FC = ({ + count, + totalCount, + title, + color = 'default', +}) => { + return ( +
+
+ {count}/{totalCount} +
+
{title}
+
+ ); +}; + +const Missions = () => { + const dispatch = useAppDispatch(); + const missions = useAppSelector((state) => state.missions.missions); + const status = useAppSelector((state) => state.missions.statuses.fetchMy); + + useEffect(() => { + dispatch(setMenuActiveProfilePage('missions')); + dispatch(fetchMyMissions()); + }, []); + + useEffect(() => { + dispatch(setMissionsStatus({ key: 'fetchMy', status: 'idle' })); + }, [status]); + + return ( +
+
+
+
+ Решенные задачи +
+
+
+ +
+
+ + + +
+
+
+ Компетенции +
+ +
+ + + +
+
+
+ +
+
+
+ ); +}; + +export default Missions; diff --git a/src/views/home/account/missions/MissionsBlock.tsx b/src/views/home/account/missions/MissionsBlock.tsx new file mode 100644 index 0000000..b9b2efe --- /dev/null +++ b/src/views/home/account/missions/MissionsBlock.tsx @@ -0,0 +1,71 @@ +import { useState, FC } from 'react'; +import { cn } from '../../../../lib/cn'; +import { ChevroneDown } from '../../../../assets/icons/groups'; +import MyMissionItem from './MyMissionItem'; +import { Mission } from '../../../../redux/slices/missions'; + +interface MissionsBlockProps { + missions: Mission[]; + title: string; + className?: string; +} + +const MissionsBlock: FC = ({ + missions, + title, + className, +}) => { + const [active, setActive] = useState(true); + + return ( +
+
{ + setActive(!active); + }} + > + {title} + +
+
+
+
+ {missions.map((v, i) => ( + + ))} +
+
+
+
+ ); +}; + +export default MissionsBlock; diff --git a/src/views/home/account/missions/MyMissionItem.tsx b/src/views/home/account/missions/MyMissionItem.tsx new file mode 100644 index 0000000..5fa27f0 --- /dev/null +++ b/src/views/home/account/missions/MyMissionItem.tsx @@ -0,0 +1,89 @@ +import { cn } from '../../../../lib/cn'; +import { useNavigate } from 'react-router-dom'; +import { Edit } from '../../../../assets/icons/input'; + +export interface MissionItemProps { + id: number; + authorId?: number; + name: string; + difficulty: number; + tags?: string[]; + timeLimit?: number; + memoryLimit?: number; + createdAt?: string; + updatedAt?: string; + type?: 'first' | 'second'; + status?: 'empty' | 'success' | 'error'; +} + +export function formatMilliseconds(ms: number): string { + const rounded = Math.round(ms) / 1000; + const formatted = rounded.toString().replace(/\.?0+$/, ''); + return `${formatted} c`; +} + +export function formatBytesToMB(bytes: number): string { + const megabytes = Math.floor(bytes / (1024 * 1024)); + return `${megabytes} МБ`; +} + +const MissionItem: React.FC = ({ + id, + name, + difficulty, + timeLimit = 1000, + memoryLimit = 256 * 1024 * 1024, + type, + status, +}) => { + const navigate = useNavigate(); + const difficultyItems = ['Easy', 'Medium', 'Hard']; + const difficultyString = + difficultyItems[Math.min(Math.max(0, difficulty - 1), 2)]; + + return ( +
{ + navigate(`/mission/${id}?back=/home/account/missions`); + }} + > +
#{id}
+
{name}
+
+ стандартный ввод/вывод {formatMilliseconds(timeLimit)},{' '} + {formatBytesToMB(memoryLimit)} +
+
+ {difficultyString} +
+
+ { + e.stopPropagation(); + }} + /> +
+
+ ); +}; + +export default MissionItem; diff --git a/src/views/home/articles/Articles.tsx b/src/views/home/articles/Articles.tsx index 13519fd..5c3caad 100644 --- a/src/views/home/articles/Articles.tsx +++ b/src/views/home/articles/Articles.tsx @@ -5,52 +5,86 @@ import ArticleItem from './ArticleItem'; import { setMenuActivePage } from '../../../redux/slices/store'; import { useNavigate } from 'react-router-dom'; import { fetchArticles } from '../../../redux/slices/articles'; - -export interface Article { - id: number; - name: string; - tags: string[]; -} +import Filters from './Filter'; const Articles = () => { const dispatch = useAppDispatch(); const navigate = useNavigate(); - const articles = useAppSelector((state) => state.articles.articles); - const status = useAppSelector((state) => state.articles.statuses.fetchAll); + // ✅ Берём данные из нового состояния + const articles = useAppSelector( + (state) => state.articles.fetchArticles.articles, + ); + const status = useAppSelector( + (state) => state.articles.fetchArticles.status, + ); + const error = useAppSelector((state) => state.articles.fetchArticles.error); useEffect(() => { dispatch(setMenuActivePage('articles')); dispatch(fetchArticles({})); - }, []); + }, [dispatch]); - if (status == 'loading') return
Загрузка...
; + // ======================== + // Состояния загрузки / ошибки + // ======================== + if (status === 'loading') { + return ( +
+ Загрузка статей... +
+ ); + } + if (status === 'failed') { + return ( +
+ Ошибка при загрузке статей + {error && ( +
+ {error} +
+ )} +
+ ); + } + + // ======================== + // Основной контент + // ======================== return ( -
+
+ {/* Заголовок */}
Статьи
{ - navigate('/article/create'); - }} + onClick={() => navigate('/article/create')} text="Создать статью" className="absolute right-0" />
-
+ {/* Фильтры */} + -
- {articles.map((v, i) => ( - - ))} + {/* Список статей */} +
+ {articles.length === 0 ? ( +
+ Пока нет статей +
+ ) : ( + articles.map((v) => ) + )}
-
pages
+ {/* Пагинация (пока заглушка) */} +
+ pages +
); diff --git a/src/views/home/articles/Filter.tsx b/src/views/home/articles/Filter.tsx new file mode 100644 index 0000000..1d8a82f --- /dev/null +++ b/src/views/home/articles/Filter.tsx @@ -0,0 +1,51 @@ +import { + FilterDropDown, + FilterItem, +} from '../../../components/drop-down-list/Filter'; +import { SorterDropDown } from '../../../components/drop-down-list/Sorter'; +import { SearchInput } from '../../../components/input/SearchInput'; + +const Filters = () => { + const items: FilterItem[] = [ + { text: 'React', value: 'react' }, + { text: 'Vue', value: 'vue' }, + { text: 'Angular', value: 'angular' }, + { text: 'Svelte', value: 'svelte' }, + { text: 'Next.js', value: 'next' }, + { text: 'Nuxt', value: 'nuxt' }, + { text: 'Solid', value: 'solid' }, + { text: 'Qwik', value: 'qwik' }, + ]; + + return ( +
+ {}} placeholder="Поиск задачи" /> + + {}} + /> + + {}} + /> +
+ ); +}; + +export default Filters; diff --git a/src/views/home/auth/Login.tsx b/src/views/home/auth/Login.tsx index 548be70..a660b35 100644 --- a/src/views/home/auth/Login.tsx +++ b/src/views/home/auth/Login.tsx @@ -1,8 +1,9 @@ +// src/views/home/auth/Login.tsx import { useState, useEffect } from 'react'; import { PrimaryButton } from '../../../components/button/PrimaryButton'; import { Input } from '../../../components/input/Input'; import { useAppDispatch, useAppSelector } from '../../../redux/hooks'; -import { Link, useNavigate } from 'react-router-dom'; +import { Link, useLocation, useNavigate } from 'react-router-dom'; import { loginUser } from '../../../redux/slices/auth'; // import { cn } from "../../../lib/cn"; import { setMenuActivePage } from '../../../redux/slices/store'; @@ -13,6 +14,7 @@ import { googleLogo } from '../../../assets/icons/input'; const Login = () => { const dispatch = useAppDispatch(); const navigate = useNavigate(); + const location = useLocation(); const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); @@ -25,12 +27,14 @@ const Login = () => { // После успешного логина useEffect(() => { dispatch(setMenuActivePage('account')); - console.log(submitClicked); + submitClicked; }, []); useEffect(() => { if (jwt) { - navigate('/home/account'); // или другая страница после входа + const from = location.state?.from; + const path = from ? from.pathname + from.search : '/home/account'; + navigate(path, { replace: true }); } }, [jwt]); diff --git a/src/views/home/auth/Register.tsx b/src/views/home/auth/Register.tsx index d341b39..4eee15a 100644 --- a/src/views/home/auth/Register.tsx +++ b/src/views/home/auth/Register.tsx @@ -1,8 +1,9 @@ +// src/views/home/auth/Register.tsx import { useState, useEffect } from 'react'; import { PrimaryButton } from '../../../components/button/PrimaryButton'; import { Input } from '../../../components/input/Input'; import { useAppDispatch, useAppSelector } from '../../../redux/hooks'; -import { useNavigate } from 'react-router-dom'; +import { useLocation, useNavigate } from 'react-router-dom'; import { registerUser } from '../../../redux/slices/auth'; // import { cn } from "../../../lib/cn"; import { setMenuActivePage } from '../../../redux/slices/store'; @@ -15,6 +16,7 @@ import { googleLogo } from '../../../assets/icons/input'; const Register = () => { const dispatch = useAppDispatch(); const navigate = useNavigate(); + const location = useLocation(); const [username, setUsername] = useState(''); const [email, setEmail] = useState(''); @@ -32,9 +34,11 @@ const Register = () => { useEffect(() => { if (jwt) { - navigate('/home/account'); + const from = location.state?.from; + const path = from ? from.pathname + from.search : '/home/account'; + navigate(path, { replace: true }); } - console.log(submitClicked); + submitClicked; }, [jwt]); const handleRegister = () => { diff --git a/src/views/home/contest/Contest.tsx b/src/views/home/contest/Contest.tsx index d184dd4..5a026bc 100644 --- a/src/views/home/contest/Contest.tsx +++ b/src/views/home/contest/Contest.tsx @@ -4,6 +4,7 @@ import { setMenuActivePage } from '../../../redux/slices/store'; import { Navigate, Route, Routes, useParams } from 'react-router-dom'; import { fetchContestById } from '../../../redux/slices/contests'; import ContestMissions from './Missions'; +import Submissions from './Submissions'; export interface Article { id: number; @@ -15,11 +16,13 @@ const Contest = () => { const { contestId } = useParams<{ contestId: string }>(); const contestIdNumber = contestId && /^\d+$/.test(contestId) ? parseInt(contestId, 10) : null; - if (contestIdNumber === null) { + if (!contestIdNumber) { return ; } const dispatch = useAppDispatch(); - const contest = useAppSelector((state) => state.contests.selectedContest); + const contest = useAppSelector( + (state) => state.contests.fetchContestById.contest, + ); useEffect(() => { dispatch(setMenuActivePage('contest')); @@ -30,8 +33,12 @@ const Contest = () => { }, [contestIdNumber]); return ( -
+
+ } + /> } diff --git a/src/views/home/contest/MissionItem.tsx b/src/views/home/contest/MissionItem.tsx index ee9579b..bf5f29e 100644 --- a/src/views/home/contest/MissionItem.tsx +++ b/src/views/home/contest/MissionItem.tsx @@ -4,12 +4,13 @@ import { useNavigate } from 'react-router-dom'; import { useLocation } from 'react-router-dom'; export interface MissionItemProps { + contestId: number; id: number; name: string; timeLimit?: number; memoryLimit?: number; type?: 'first' | 'second'; - status?: 'empty' | 'success' | 'error'; + status?: 'success' | 'error'; } export function formatMilliseconds(ms: number): string { @@ -24,6 +25,7 @@ export function formatBytesToMB(bytes: number): string { } const MissionItem: React.FC = ({ + contestId, id, name, timeLimit = 1000, @@ -48,7 +50,7 @@ const MissionItem: React.FC = ({ 'cursor-pointer brightness-100 hover:brightness-125 transition-all duration-300', )} onClick={() => { - navigate(`/mission/${id}?back=${path}`); + navigate(`/mission/${id}?back=${path}&contestId=${contestId}`); }} >
#{id}
diff --git a/src/views/home/contest/Missions.tsx b/src/views/home/contest/Missions.tsx index 535cf05..1893df9 100644 --- a/src/views/home/contest/Missions.tsx +++ b/src/views/home/contest/Missions.tsx @@ -1,43 +1,124 @@ -import { FC } from 'react'; -import MissionItem from './MissionItem'; -import { Contest } from '../../../redux/slices/contests'; +import { FC, useEffect } from "react"; +import MissionItem from "./MissionItem"; +import { + Contest, + fetchMySubmissions, + setContestStatus, +} from "../../../redux/slices/contests"; +import { useAppDispatch, useAppSelector } from "../../../redux/hooks"; +import { PrimaryButton } from "../../../components/button/PrimaryButton"; +import { useNavigate } from "react-router-dom"; +import { arrowLeft } from "../../../assets/icons/header"; export interface Article { - id: number; - name: string; - tags: string[]; + id: number; + name: string; + tags: string[]; } interface ContestMissionsProps { - contest: Contest | null; + contest?: Contest; } const ContestMissions: FC = ({ contest }) => { - if (!contest) { - return <>; - } + const navigate = useNavigate(); + const dispatch = useAppDispatch(); + const { submissions, status } = useAppSelector( + (state) => state.contests.fetchMySubmissions + ); - return ( -
-
-
-
- {contest?.name} {contest.id} -
-
- {contest.missions.map((v, i) => ( - - ))} -
-
+ useEffect(() => { + if (contest) dispatch(fetchMySubmissions(contest.id)); + }, [contest]); + + useEffect(() => { + if (status == "successful") { + dispatch(setContestStatus({ key: "fetchMySubmissions", status: "idle" })); + } + }, [status]); + + if (!contest) { + return <>; + } + + const solvedCount = (contest.missions ?? []).filter((mission) => + submissions.some( + (s) => + s.solution.missionId === mission.id && + s.solution.status === "Accepted: All tests passed" + ) + ).length; + + const totalCount = contest.missions?.length ?? 0; + + return ( +
+
+
+ {contest.name}
- ); +
+
+ { + navigate(`/home/contests`); + }} + /> + + Контест #{contest.id} + +
+
{contest.attemptDurationMinutes ?? 0} минут
+
+
+
+
{`${solvedCount}/${totalCount} Решено`}
+ { + navigate(`/contest/${contest.id}/submissions`); + }} + text="Мои посылки" + /> +
+ +
+
+ {(contest.missions ?? []).map((v, i) => { + const missionSubmissions = submissions.filter( + (s) => s.solution.missionId === v.id + ); + + const hasSuccess = missionSubmissions.some( + (s) => s.solution.status == "Accepted: All tests passed" + ); + + console.log(missionSubmissions); + + const status = hasSuccess + ? "success" + : missionSubmissions.length > 0 + ? "error" + : undefined; + + return ( + + ); + })} +
+
+
+ ); }; export default ContestMissions; diff --git a/src/views/home/contest/SubmissionItem.tsx b/src/views/home/contest/SubmissionItem.tsx new file mode 100644 index 0000000..6d90537 --- /dev/null +++ b/src/views/home/contest/SubmissionItem.tsx @@ -0,0 +1,94 @@ +import { cn } from '../../../lib/cn'; +// import { IconError, IconSuccess } from "../../../assets/icons/missions"; +// import { useNavigate } from "react-router-dom"; + +export interface SubmissionItemProps { + id: number; + datetime: string; + missionId: number; + language: string; + verdict: string; + duration: number; + memory: number; + type: 'first' | 'second'; + status?: 'success' | 'wronganswer' | 'timelimit'; +} + +export function formatMilliseconds(ms: number): string { + const rounded = Math.round(ms) / 1000; + const formatted = rounded.toString().replace(/\.?0+$/, ''); + return `${formatted} c`; +} + +export function formatBytesToMB(bytes: number): string { + const megabytes = Math.floor(bytes / (1024 * 1024)); + return `${megabytes} МБ`; +} + +function formatDate(dateString: string): string { + const date = new Date(dateString); + + const day = date.getDate().toString().padStart(2, '0'); + const month = (date.getMonth() + 1).toString().padStart(2, '0'); + const year = date.getFullYear(); + + const hours = date.getHours().toString().padStart(2, '0'); + const minutes = date.getMinutes().toString().padStart(2, '0'); + + return `${day}/${month}/${year}\n${hours}:${minutes}`; +} + +const SubmissionItem: React.FC = ({ + id, + datetime, + missionId, + language, + verdict, + duration, + memory, + type, + status +}) => { + // const navigate = useNavigate(); + + return ( +
{}} + > +
#{id}
+
+ {formatDate(datetime)} +
+
{missionId}
+
{language}
+
+ {verdict} +
+
{formatMilliseconds(duration)}
+
+ {formatBytesToMB(memory)} +
+
+ ); +}; + +export default SubmissionItem; diff --git a/src/views/home/contest/Submissions.tsx b/src/views/home/contest/Submissions.tsx index e69de29..7143099 100644 --- a/src/views/home/contest/Submissions.tsx +++ b/src/views/home/contest/Submissions.tsx @@ -0,0 +1,129 @@ +import SubmissionItem from "./SubmissionItem"; +import { useAppDispatch, useAppSelector } from "../../../redux/hooks"; +import { FC, useEffect } from "react"; +import { + Contest, + fetchMySubmissions, + setContestStatus, +} from "../../../redux/slices/contests"; +import { arrowLeft } from "../../../assets/icons/header"; +import { useNavigate } from "react-router-dom"; + +export interface Mission { + id: number; + authorId: number; + name: string; + difficulty: "Easy" | "Medium" | "Hard"; + tags: string[]; + timeLimit: number; + memoryLimit: number; + createdAt: string; + updatedAt: string; +} + +interface SubmissionsProps { + contest: Contest; +} + +const Submissions: FC = ({ contest }) => { + const dispatch = useAppDispatch(); + const navigate = useNavigate(); + + const { submissions, status } = useAppSelector( + (state) => state.contests.fetchMySubmissions + ); + + useEffect(() => { + if (contest && contest.id) dispatch(fetchMySubmissions(contest.id)); + }, [contest]); + + useEffect(() => { + if (status == "successful") { + dispatch(setContestStatus({ key: "fetchMySubmissions", status: "idle" })); + } + }, [status]); + + const checkStatus = (status: string) => { + if (status == "IncorrectAnswer") return "wronganswer"; + if (status == "TimeLimitError") return "timelimit"; + return undefined; + }; + + const solvedCount = (contest.missions ?? []).filter((mission) => + submissions.some( + (s) => + s.solution.missionId === mission.id && + s.solution.status === "Accepted: All tests passed" + ) + ).length; + + const totalCount = contest.missions?.length ?? 0; + + return ( +
+
+
+ {contest.name} +
+
+
+ { + navigate(`/contest/${contest.id}`); + }} + /> + + Контест #{contest.id} + +
+
{`${solvedCount}/${totalCount} Решено`}
+
+
+ +
+
+
Посылка
+
Когда
+
Задача
+
Язык
+
Вердикт
+
Время
+
Память
+
+ + {!submissions || submissions.length == 0 ? ( +
Вы еще ничего не отсылали
+ ) : ( + <> + {submissions.map((v, i) => ( + + ))} + + )} +
+
+ ); +}; + +export default Submissions; diff --git a/src/views/home/contests/ContestItem.tsx b/src/views/home/contests/ContestItem.tsx index c2fc6fd..1abbf4a 100644 --- a/src/views/home/contests/ContestItem.tsx +++ b/src/views/home/contests/ContestItem.tsx @@ -98,22 +98,12 @@ const ContestItem: React.FC = ({ {statusRegister == 'reg' ? ( <> {' '} - { - e.stopPropagation(); - }} - text="Регистрация" - /> + {}} text="Регистрация" /> ) : ( <> {' '} - { - e.stopPropagation(); - }} - text="Вы записаны" - /> + {}} text="Вы записаны" /> )}
diff --git a/src/views/home/contests/Contests.tsx b/src/views/home/contests/Contests.tsx index d3ad1eb..7e33094 100644 --- a/src/views/home/contests/Contests.tsx +++ b/src/views/home/contests/Contests.tsx @@ -6,6 +6,7 @@ import ContestsBlock from './ContestsBlock'; import { setMenuActivePage } from '../../../redux/slices/store'; import { fetchContests } from '../../../redux/slices/contests'; import ModalCreateContest from './ModalCreate'; +import Filters from './Filter'; const Contests = () => { const dispatch = useAppDispatch(); @@ -14,9 +15,13 @@ const Contests = () => { const [modalActive, setModalActive] = useState(false); // Берём данные из Redux - const contests = useAppSelector((state) => state.contests.contests); - const status = useAppSelector((state) => state.contests.statuses.create); - const error = useAppSelector((state) => state.contests.error); + const contests = useAppSelector( + (state) => state.contests.fetchContests.contests, + ); + const status = useAppSelector( + (state) => state.contests.fetchContests.status, + ); + const error = useAppSelector((state) => state.contests.fetchContests.error); // При загрузке страницы — выставляем активную вкладку и подгружаем контесты useEffect(() => { @@ -24,16 +29,6 @@ const Contests = () => { dispatch(fetchContests({})); }, []); - if (status == 'loading') { - return ( -
Загрузка контестов...
- ); - } - - if (error) { - return
Ошибка: {error}
; - } - return (
@@ -54,25 +49,40 @@ const Contests = () => { />
-
+ + {status == 'loading' && ( +
+ Загрузка контестов... +
+ )} + {status == 'failed' && ( +
Ошибка: {error}
+ )} + {status == 'successful' && ( + <> + { + const endTime = new Date( + contest.endsAt ?? new Date().toDateString(), + ).getTime(); + return endTime >= now.getTime(); + })} + /> - { - const endTime = new Date(contest.endsAt).getTime(); - return endTime >= now.getTime(); - })} - /> - - { - const endTime = new Date(contest.endsAt).getTime(); - return endTime < now.getTime(); - })} - /> + { + const endTime = new Date( + contest.endsAt ?? new Date().toDateString(), + ).getTime(); + return endTime < now.getTime(); + })} + /> + + )}
= ({ key={i} id={v.id} name={v.name} - startAt={v.startsAt} + startAt={v.startsAt ?? new Date().toString()} statusRegister={'reg'} duration={ - new Date(v.endsAt).getTime() - - new Date(v.startsAt).getTime() + new Date(v.endsAt ?? new Date().toString()).getTime() - + new Date(v.startsAt ?? new Date().toString()).getTime() } - members={v.members.length} + members={v.members?.length ?? 0} type={i % 2 ? 'second' : 'first'} /> ))} diff --git a/src/views/home/contests/Filter.tsx b/src/views/home/contests/Filter.tsx new file mode 100644 index 0000000..ca01a9d --- /dev/null +++ b/src/views/home/contests/Filter.tsx @@ -0,0 +1,51 @@ +import { + FilterDropDown, + FilterItem, +} from '../../../components/drop-down-list/Filter'; +import { SorterDropDown } from '../../../components/drop-down-list/Sorter'; +import { SearchInput } from '../../../components/input/SearchInput'; + +const Filters = () => { + const items: FilterItem[] = [ + { text: 'React', value: 'react' }, + { text: 'Vue', value: 'vue' }, + { text: 'Angular', value: 'angular' }, + { text: 'Svelte', value: 'svelte' }, + { text: 'Next.js', value: 'next' }, + { text: 'Nuxt', value: 'nuxt' }, + { text: 'Solid', value: 'solid' }, + { text: 'Qwik', value: 'qwik' }, + ]; + + return ( +
+ {}} placeholder="Поиск задачи" /> + + console.log(v)} + /> + + console.log(values)} + /> +
+ ); +}; + +export default Filters; diff --git a/src/views/home/contests/ModalCreate.tsx b/src/views/home/contests/ModalCreate.tsx index ac9cc1c..0c30465 100644 --- a/src/views/home/contests/ModalCreate.tsx +++ b/src/views/home/contests/ModalCreate.tsx @@ -4,9 +4,13 @@ import { PrimaryButton } from '../../../components/button/PrimaryButton'; import { SecondaryButton } from '../../../components/button/SecondaryButton'; import { Input } from '../../../components/input/Input'; import { useAppDispatch, useAppSelector } from '../../../redux/hooks'; -import { createContest } from '../../../redux/slices/contests'; +import { + createContest, + setContestStatus, +} from '../../../redux/slices/contests'; import { CreateContestBody } from '../../../redux/slices/contests'; import DateRangeInput from '../../../components/input/DateRangeInput'; +import { useNavigate } from 'react-router-dom'; interface ModalCreateContestProps { active: boolean; @@ -18,28 +22,37 @@ const ModalCreateContest: FC = ({ setActive, }) => { const dispatch = useAppDispatch(); - const status = useAppSelector((state) => state.contests.statuses.create); + const navigate = useNavigate(); + const status = useAppSelector( + (state) => state.contests.createContest.status, + ); const [form, setForm] = useState({ name: '', description: '', scheduleType: 'AlwaysOpen', visibility: 'Public', - startsAt: null, - endsAt: null, - attemptDurationMinutes: null, - maxAttempts: null, + startsAt: '', + endsAt: '', + attemptDurationMinutes: 0, + maxAttempts: 0, allowEarlyFinish: false, - groupId: null, - missionIds: null, - articleIds: null, - participantIds: null, - organizerIds: null, + missionIds: [], + articleIds: [], }); + const contest = useAppSelector( + (state) => state.contests.createContest.contest, + ); + useEffect(() => { if (status === 'successful') { - setActive(false); + dispatch( + setContestStatus({ key: 'createContest', status: 'idle' }), + ); + navigate( + `/contest/create?back=/home/account/contests&contestId=${contest.id}`, + ); } }, [status]); @@ -174,7 +187,9 @@ const ModalCreateContest: FC = ({ {/* Кнопки */}
{ + handleSubmit(); + }} text="Создать" disabled={status === 'loading'} /> diff --git a/src/views/home/group/Group.tsx b/src/views/home/group/Group.tsx new file mode 100644 index 0000000..ef34d56 --- /dev/null +++ b/src/views/home/group/Group.tsx @@ -0,0 +1,52 @@ +import { FC, useEffect } from 'react'; +import { cn } from '../../../lib/cn'; +import { useParams, Navigate, Routes, Route } from 'react-router-dom'; +import { useAppDispatch, useAppSelector } from '../../../redux/hooks'; +import { fetchGroupById } from '../../../redux/slices/groups'; +import GroupMenu from './GroupMenu'; +import { Posts } from './posts/Posts'; +import { SearchInput } from '../../../components/input/SearchInput'; +import { Chat } from './chat/Chat'; +import { Contests } from './contests/Contests'; + +interface GroupsBlockProps {} + +const Group: FC = () => { + const groupId = Number(useParams<{ groupId: string }>().groupId); + if (!groupId) { + return ; + } + + const dispatch = useAppDispatch(); + const group = useAppSelector((state) => state.groups.fetchGroupById.group); + + useEffect(() => { + dispatch(fetchGroupById(groupId)); + }, [groupId]); + + console.log(group); + + return ( +
+
{group?.name}
+ + + + + } /> + } /> + } /> + } + /> + +
+ ); +}; + +export default Group; diff --git a/src/views/home/group/GroupMenu.tsx b/src/views/home/group/GroupMenu.tsx new file mode 100644 index 0000000..081c9f2 --- /dev/null +++ b/src/views/home/group/GroupMenu.tsx @@ -0,0 +1,96 @@ +import { MessageChat, Home, Cup } from '../../../assets/icons/group'; + +import React, { FC } from 'react'; +import { Link } from 'react-router-dom'; +import { useAppDispatch, useAppSelector } from '../../../redux/hooks'; +import { + setMenuActivePage, + setMenuActiveProfilePage, +} from '../../../redux/slices/store'; + +interface MenuItemProps { + icon: string; + text: string; + href: string; + page: string; + profilePage: string; + active?: boolean; +} + +const MenuItem: React.FC = ({ + icon, + text = '', + href = '', + active = false, + page = '', + profilePage = '', +}) => { + const dispatch = useAppDispatch(); + + return ( + { + dispatch(setMenuActivePage(page)); + dispatch(setMenuActiveProfilePage(profilePage)); + }} + > + + {text} + + ); +}; + +interface GroupMenuProps { + groupId: number; +} + +const GroupMenu: FC = ({ groupId }) => { + const menuItems = [ + { + text: 'Главная', + href: `/group/${groupId}/home`, + icon: Home, + page: 'group', + profilePage: 'home', + }, + { + text: 'Чат', + href: `/group/${groupId}/chat`, + icon: MessageChat, + page: 'group', + profilePage: 'chat', + }, + { + text: 'Контесты', + href: `/group/${groupId}/contests`, + icon: Cup, + page: 'group', + profilePage: 'contests', + }, + ]; + + const activeGroupPage = useAppSelector( + (state) => state.store.menu.activeGroupPage, + ); + + return ( +
+ {menuItems.map((v, i) => ( + + ))} +
+ ); +}; + +export default GroupMenu; diff --git a/src/views/home/group/chat/Chat.tsx b/src/views/home/group/chat/Chat.tsx new file mode 100644 index 0000000..7464e22 --- /dev/null +++ b/src/views/home/group/chat/Chat.tsx @@ -0,0 +1,12 @@ +import { useEffect } from 'react'; +import { useAppDispatch } from '../../../../redux/hooks'; +import { setMenuActiveGroupPage } from '../../../../redux/slices/store'; + +export const Chat = () => { + const dispatch = useAppDispatch(); + + useEffect(() => { + dispatch(setMenuActiveGroupPage('chat')); + }, []); + return <>; +}; diff --git a/src/views/home/group/contests/Contests.tsx b/src/views/home/group/contests/Contests.tsx new file mode 100644 index 0000000..39faea4 --- /dev/null +++ b/src/views/home/group/contests/Contests.tsx @@ -0,0 +1,12 @@ +import { useEffect } from 'react'; +import { useAppDispatch } from '../../../../redux/hooks'; +import { setMenuActiveGroupPage } from '../../../../redux/slices/store'; + +export const Contests = () => { + const dispatch = useAppDispatch(); + + useEffect(() => { + dispatch(setMenuActiveGroupPage('contests')); + }, []); + return <>; +}; diff --git a/src/views/home/group/posts/ModalCreate.tsx b/src/views/home/group/posts/ModalCreate.tsx new file mode 100644 index 0000000..39882ae --- /dev/null +++ b/src/views/home/group/posts/ModalCreate.tsx @@ -0,0 +1,72 @@ +import { FC, useEffect, useState } from 'react'; +import { Modal } from '../../../../components/modal/Modal'; +import { PrimaryButton } from '../../../../components/button/PrimaryButton'; +import { SecondaryButton } from '../../../../components/button/SecondaryButton'; +import { Input } from '../../../../components/input/Input'; +import { useAppDispatch, useAppSelector } from '../../../../redux/hooks'; +import { createGroup } from '../../../../redux/slices/groups'; +import MarkdownEditor from '../../../articleeditor/Editor'; +import { + createPost, + setGroupFeedStatus, +} from '../../../../redux/slices/groupfeed'; + +interface ModalCreateProps { + groupId: number; + active: boolean; + setActive: (value: boolean) => void; +} + +const ModalCreate: FC = ({ active, setActive, groupId }) => { + // const [name, setName] = useState(''); + const [content, setContent] = useState(''); + const status = useAppSelector((state) => state.groupfeed.createPost.status); + const dispatch = useAppDispatch(); + + useEffect(() => { + if (status == 'successful') { + setActive(false); + dispatch(setGroupFeedStatus({ key: 'createPost', status: 'idle' })); + } + }, [status]); + + return ( + +
+
Создать пост
+ +
+ { + setContent(v); + }} + /> +
+
+ { + dispatch( + createPost({ name: '', content, groupId }), + ); + }} + text={status == 'idle' ? 'Опубликовать' : 'Загрузка...'} + disabled={status == 'loading'} + /> + { + setActive(false); + }} + text="Отмена" + /> +
+
+
+ ); +}; + +export default ModalCreate; diff --git a/src/views/home/group/posts/ModalUpdate.tsx b/src/views/home/group/posts/ModalUpdate.tsx new file mode 100644 index 0000000..55ada02 --- /dev/null +++ b/src/views/home/group/posts/ModalUpdate.tsx @@ -0,0 +1,140 @@ +import { FC, useEffect, useState } from 'react'; +import { Modal } from '../../../../components/modal/Modal'; +import { PrimaryButton } from '../../../../components/button/PrimaryButton'; +import { SecondaryButton } from '../../../../components/button/SecondaryButton'; +import { Input } from '../../../../components/input/Input'; +import { useAppDispatch, useAppSelector } from '../../../../redux/hooks'; +import { createGroup } from '../../../../redux/slices/groups'; +import MarkdownEditor, { MarkDownPattern } from '../../../articleeditor/Editor'; +import { + createPost, + deletePost, + fetchPostById, + setGroupFeedStatus, + updatePost, +} from '../../../../redux/slices/groupfeed'; +import { ReverseButton } from '../../../../components/button/ReverseButton'; +import { cn } from '../../../../lib/cn'; + +interface ModalUpdateProps { + groupId: number; + postId: number; + active: boolean; + setActive: (value: boolean) => void; +} + +const ModalUpdate: FC = ({ + active, + setActive, + groupId, + postId, +}) => { + // const [name, setName] = useState(''); + const [content, setContent] = useState(''); + const status = useAppSelector((state) => state.groupfeed.updatePost.status); + const statusDelete = useAppSelector( + (state) => state.groupfeed.deletePost.status, + ); + const { post, status: statusPost } = useAppSelector( + (state) => state.groupfeed.fetchPostById, + ); + const dispatch = useAppDispatch(); + + useEffect(() => { + if (status == 'successful') { + setActive(false); + dispatch(setGroupFeedStatus({ key: 'updatePost', status: 'idle' })); + } + }, [status]); + + useEffect(() => { + if (statusDelete == 'successful') { + setActive(false); + dispatch(setGroupFeedStatus({ key: 'deletePost', status: 'idle' })); + } + }, [statusDelete]); + + useEffect(() => { + dispatch(fetchPostById({ groupId, postId })); + }, [postId]); + + return ( + +
+
+ Обновить пост #{post?.id} +
+
+
Загрузка...
+
+
+ { + setContent(v); + }} + /> +
+
+ { + dispatch( + updatePost({ + name: '', + content, + groupId, + postId, + }), + ); + }} + text={status == 'idle' ? 'Сохранить' : 'Загрузка...'} + disabled={ + status == 'loading' || statusPost != 'successful' + } + /> + { + dispatch(deletePost({ groupId, postId })); + }} + color="error" + text={ + statusDelete == 'idle' ? 'Удалить' : 'Загрузка...' + } + disabled={ + statusDelete == 'loading' || + statusPost != 'successful' + } + /> + { + setActive(false); + }} + text="Отмена" + /> +
+
+
+ ); +}; + +export default ModalUpdate; diff --git a/src/views/home/group/posts/PostItem.tsx b/src/views/home/group/posts/PostItem.tsx new file mode 100644 index 0000000..98e34d9 --- /dev/null +++ b/src/views/home/group/posts/PostItem.tsx @@ -0,0 +1,86 @@ +import { FC } from 'react'; +import { useAppSelector } from '../../../../redux/hooks'; +import MarkdownPreview from '../../../articleeditor/MarckDownPreview'; +import { Edit } from '../../../../assets/icons/input'; + +function convertDate(isoString: string) { + const date = new Date(isoString); + + const dd = String(date.getUTCDate()).padStart(2, '0'); + const mm = String(date.getUTCMonth() + 1).padStart(2, '0'); + const yyyy = date.getUTCFullYear(); + + const hh = String(date.getUTCHours()).padStart(2, '0'); + const min = String(date.getUTCMinutes()).padStart(2, '0'); + + return `${dd}.${mm}.${yyyy} ${hh}:${min}`; +} + +interface PostItemProps { + id: number; + groupId: number; + authorId: number; + authorUsername: string; + name: string; + content: string; + createdAt: string; + updatedAt: string; + isAdmin: boolean; + setModalUpdateActive: (v: boolean) => void; + setUpdatePostId: (v: number) => void; +} + +export const PostItem: FC = ({ + id, + groupId, + authorId, + authorUsername, + name, + content, + createdAt, + updatedAt, + isAdmin, + setModalUpdateActive, + setUpdatePostId, +}) => { + const members = useAppSelector( + (state) => state.groups.fetchGroupById.group?.members, + ); + const member = members?.find((m) => m.userId === authorId); + + return ( +
+
+
+
+
{authorUsername}
+
+ {member ? member.role : 'роль не найдена'} +
+
+
+
+ {convertDate(createdAt)} +
+
+ + {isAdmin && ( +
{ + setUpdatePostId(id); + setModalUpdateActive(true); + }} + > + +
+ )} +
+ +
+ +
+
+ ); +}; diff --git a/src/views/home/group/posts/Posts.tsx b/src/views/home/group/posts/Posts.tsx new file mode 100644 index 0000000..e5cc012 --- /dev/null +++ b/src/views/home/group/posts/Posts.tsx @@ -0,0 +1,111 @@ +import { FC, useEffect, useState } from 'react'; + +import { useAppSelector, useAppDispatch } from '../../../../redux/hooks'; +import { fetchGroupPosts } from '../../../../redux/slices/groupfeed'; +import { SearchInput } from '../../../../components/input/SearchInput'; +import { setMenuActiveGroupPage } from '../../../../redux/slices/store'; +import { fetchGroupById } from '../../../../redux/slices/groups'; +import { SecondaryButton } from '../../../../components/button/SecondaryButton'; +import ModalCreate from './ModalCreate'; +import { PostItem } from './PostItem'; +import ModalUpdate from './ModalUpdate'; + +interface PostsProps { + groupId: number; +} + +export const Posts: FC = ({ groupId }) => { + const dispatch = useAppDispatch(); + + const [modalCreateActive, setModalCreateActive] = useState(false); + const [modalUpdateActive, setModalUpdateActive] = useState(false); + const [updatePostId, setUpdatePostId] = useState(0); + const [isAdmin, setIsAdmin] = useState(false); + const { pages, status } = useAppSelector( + (state) => state.groupfeed.fetchPosts, + ); + const { id: userId } = useAppSelector((state) => state.auth); + + const { group } = useAppSelector((state) => state.groups.fetchGroupById); + + // Загружаем только первую страницу + useEffect(() => { + dispatch(fetchGroupPosts({ groupId, page: 0, pageSize: 20 })); + dispatch(fetchGroupById(groupId)); + }, [groupId]); + + useEffect(() => { + dispatch(setMenuActiveGroupPage('home')); + }, []); + + useEffect(() => { + if (!group) return; + + const isUserAdmin = + group.members?.some( + (m) => + Number(m.userId) === Number(userId) && + m.role.includes('Administrator'), + ) || false; + + setIsAdmin(isUserAdmin); + }, [group, userId]); + + const page0 = pages[0]; + + return ( +
+
+ {}} + placeholder="Поиск сообщений" + /> + {isAdmin && ( +
+ { + setModalCreateActive(true); + }} + text="Создать пост" + /> +
+ )} +
+ + {status === 'loading' &&
Загрузка...
} + {status === 'failed' &&
Ошибка загрузки постов
} + + {status == 'successful' && + page0?.items && + page0.items.length > 0 ? ( +
+ {page0.items.map((post, i) => ( + + ))} +
+ ) : status === 'successful' ? ( +
Постов пока нет
+ ) : null} + + + + +
+ ); +}; diff --git a/src/views/home/groupinviter/GroupInvite.tsx b/src/views/home/groupinviter/GroupInvite.tsx new file mode 100644 index 0000000..1b3cc7c --- /dev/null +++ b/src/views/home/groupinviter/GroupInvite.tsx @@ -0,0 +1,109 @@ +import { useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useAppDispatch, useAppSelector } from '../../../redux/hooks'; +import { setMenuActivePage } from '../../../redux/slices/store'; +import { useQuery } from '../../../hooks/useQuery'; +import { PrimaryButton } from '../../../components/button/PrimaryButton'; +import { SecondaryButton } from '../../../components/button/SecondaryButton'; +import { + joinGroupByToken, + setGroupsStatus, +} from '../../../redux/slices/groups'; + +const GroupInvite = () => { + const dispatch = useAppDispatch(); + const navigate = useNavigate(); + + const query = useQuery(); + const token = query.get('token') ?? undefined; + const expiresAt = query.get('expiresAt') ?? undefined; + const groupName = query.get('groupName') ?? undefined; + const groupId = Number(query.get('groupId') ?? undefined); + + const username = useAppSelector((state) => state.auth.username); + const joinStatus = useAppSelector( + (state) => state.groups.joinGroupByToken.status, + ); + const joinError = useAppSelector( + (state) => state.groups.joinGroupByToken.error, + ); + + useEffect(() => { + dispatch(setMenuActivePage('groups')); + }, []); + + useEffect(() => { + if (joinStatus == 'successful') { + dispatch( + setGroupsStatus({ key: 'joinGroupByToken', status: 'idle' }), + ); + navigate(`/group/${groupId}`); + } + }, [joinStatus]); + + if (!token || !expiresAt || !groupName || !groupId) { + return ( +
+ Приглашение признано недействительным. +
+ ); + } + + const isExpired = new Date(expiresAt) < new Date(); + + if (isExpired) { + return ( +
+ Период действия приглашения истек. +
+ ); + } + + const handleJoin = async () => { + if (!token) return; + try { + await dispatch(joinGroupByToken(token)).unwrap(); + } catch (err) {} + }; + + const handleCancel = () => { + navigate('/home/account'); + }; + + return ( +
+
+
+ Привет, {username}! +
+
+ Вы действительно хотите присоединиться к группе: +
+
+ "{groupName}" +
+ + {joinError && ( +
+ Ошибка присоединения: {joinError} +
+ )} + +
+ + +
+
+
+ ); +}; + +export default GroupInvite; diff --git a/src/views/home/groups/Filter.tsx b/src/views/home/groups/Filter.tsx new file mode 100644 index 0000000..1d8a82f --- /dev/null +++ b/src/views/home/groups/Filter.tsx @@ -0,0 +1,51 @@ +import { + FilterDropDown, + FilterItem, +} from '../../../components/drop-down-list/Filter'; +import { SorterDropDown } from '../../../components/drop-down-list/Sorter'; +import { SearchInput } from '../../../components/input/SearchInput'; + +const Filters = () => { + const items: FilterItem[] = [ + { text: 'React', value: 'react' }, + { text: 'Vue', value: 'vue' }, + { text: 'Angular', value: 'angular' }, + { text: 'Svelte', value: 'svelte' }, + { text: 'Next.js', value: 'next' }, + { text: 'Nuxt', value: 'nuxt' }, + { text: 'Solid', value: 'solid' }, + { text: 'Qwik', value: 'qwik' }, + ]; + + return ( +
+ {}} placeholder="Поиск задачи" /> + + {}} + /> + + {}} + /> +
+ ); +}; + +export default Filters; diff --git a/src/views/home/groups/Group.tsx b/src/views/home/groups/Group.tsx deleted file mode 100644 index cc6ad00..0000000 --- a/src/views/home/groups/Group.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { FC } from 'react'; -import { cn } from '../../../lib/cn'; -import { useParams, Navigate } from 'react-router-dom'; - -interface GroupsBlockProps {} - -const Group: FC = () => { - const { groupId } = useParams<{ groupId: string }>(); - const groupIdNumber = Number(groupId); - - if (!groupId || isNaN(groupIdNumber) || !groupIdNumber) { - return ; - } - - return ( -
- {groupIdNumber} -
- ); -}; - -export default Group; diff --git a/src/views/home/groups/GroupItem.tsx b/src/views/home/groups/GroupItem.tsx index d7f579f..4a979b0 100644 --- a/src/views/home/groups/GroupItem.tsx +++ b/src/views/home/groups/GroupItem.tsx @@ -7,7 +7,7 @@ import { EyeOpen, } from '../../../assets/icons/groups'; import { useNavigate } from 'react-router-dom'; -import { GroupUpdate } from './Groups'; +import { GroupInvite, GroupUpdate } from './Groups'; export interface GroupItemProps { id: number; @@ -17,6 +17,9 @@ export interface GroupItemProps { description: string; setUpdateActive: (value: any) => void; setUpdateGroup: (value: GroupUpdate) => void; + setInviteActive: (value: any) => void; + setInviteGroup: (value: GroupInvite) => void; + type: 'manage' | 'member'; } interface IconComponentProps { @@ -45,6 +48,9 @@ const GroupItem: React.FC = ({ description, setUpdateGroup, setUpdateActive, + setInviteActive, + setInviteGroup, + type, }) => { const navigate = useNavigate(); @@ -63,10 +69,16 @@ const GroupItem: React.FC = ({
{name}
- {(role == 'menager' || role == 'owner') && ( - + {type == 'manage' && ( + { + setInviteActive(true); + setInviteGroup({ id, name }); + }} + /> )} - {(role == 'menager' || role == 'owner') && ( + {type == 'manage' && ( { diff --git a/src/views/home/groups/Groups.tsx b/src/views/home/groups/Groups.tsx index 64b3692..2752d3c 100644 --- a/src/views/home/groups/Groups.tsx +++ b/src/views/home/groups/Groups.tsx @@ -7,6 +7,8 @@ import { setMenuActivePage } from '../../../redux/slices/store'; import { fetchMyGroups } from '../../../redux/slices/groups'; import ModalCreate from './ModalCreate'; import ModalUpdate from './ModalUpdate'; +import Filters from './Filter'; +import ModalInvite from './ModalInvite'; export interface GroupUpdate { id: number; @@ -14,19 +16,35 @@ export interface GroupUpdate { description: string; } +export interface GroupInvite { + id: number; + name: string; +} + const Groups = () => { - const [modalActive, setModalActive] = useState(false); - const [modelUpdateActive, setModalUpdateActive] = useState(false); + const [modalActive, setModalActive] = useState(false); + const [modalUpdateActive, setModalUpdateActive] = useState(false); const [updateGroup, setUpdateGroup] = useState({ id: 0, name: '', description: '', }); + const [modalInviteActive, setModalInviteActive] = useState(false); + const [inviteGroup, setInviteGroup] = useState({ + id: 0, + name: '', + }); const dispatch = useAppDispatch(); - // Берём группы из стора - const groups = useAppSelector((store) => store.groups.groups); + // ✅ Берём группы и статус из нового слайса + const groups = useAppSelector((store) => store.groups.fetchMyGroups.groups); + const groupsStatus = useAppSelector( + (store) => store.groups.fetchMyGroups.status, + ); + const groupsError = useAppSelector( + (store) => store.groups.fetchMyGroups.error, + ); // Берём текущего пользователя const currentUserName = useAppSelector((store) => store.auth.username); @@ -51,8 +69,8 @@ const Groups = () => { (m) => m.username === currentUserName, ); if (!me) return; - - if (me.role === 'Administrator') { + const roles = me.role.split(',').map((r) => r.trim()); + if (roles.includes('Administrator')) { managed.push(group); } else { current.push(group); @@ -67,7 +85,7 @@ const Groups = () => { }, [groups, currentUserName]); return ( -
+
{ Группы
{ - setModalActive(true); - }} + onClick={() => setModalActive(true)} text="Создать группу" className="absolute right-0" />
-
+ - - - + {groupsStatus === 'loading' && ( +
+ Загрузка групп... +
+ )} + {groupsStatus === 'failed' && ( +
+ Ошибка: {groupsError || 'Не удалось загрузить группы'} +
+ )} + + {groupsStatus === 'successful' && ( + <> + + + + + )}
+
); }; diff --git a/src/views/home/groups/GroupsBlock.tsx b/src/views/home/groups/GroupsBlock.tsx index e66d713..2f64f17 100644 --- a/src/views/home/groups/GroupsBlock.tsx +++ b/src/views/home/groups/GroupsBlock.tsx @@ -3,7 +3,7 @@ import GroupItem from './GroupItem'; import { cn } from '../../../lib/cn'; import { ChevroneDown } from '../../../assets/icons/groups'; import { Group } from '../../../redux/slices/groups'; -import { GroupUpdate } from './Groups'; +import { GroupInvite, GroupUpdate } from './Groups'; interface GroupsBlockProps { groups: Group[]; @@ -11,6 +11,9 @@ interface GroupsBlockProps { className?: string; setUpdateActive: (value: any) => void; setUpdateGroup: (value: GroupUpdate) => void; + setInviteActive: (value: any) => void; + setInviteGroup: (value: GroupInvite) => void; + type: 'manage' | 'member'; } const GroupsBlock: FC = ({ @@ -19,6 +22,9 @@ const GroupsBlock: FC = ({ className, setUpdateActive, setUpdateGroup, + setInviteActive, + setInviteGroup, + type, }) => { const [active, setActive] = useState(title != 'Скрытые'); @@ -63,8 +69,11 @@ const GroupsBlock: FC = ({ description={v.description} setUpdateActive={setUpdateActive} setUpdateGroup={setUpdateGroup} + setInviteActive={setInviteActive} + setInviteGroup={setInviteGroup} role={'owner'} name={v.name} + type={type} /> ))}
diff --git a/src/views/home/groups/ModalCreate.tsx b/src/views/home/groups/ModalCreate.tsx index 458c491..ecda215 100644 --- a/src/views/home/groups/ModalCreate.tsx +++ b/src/views/home/groups/ModalCreate.tsx @@ -14,7 +14,7 @@ interface ModalCreateProps { const ModalCreate: FC = ({ active, setActive }) => { const [name, setName] = useState(''); const [description, setDescription] = useState(''); - const status = useAppSelector((state) => state.groups.statuses.create); + const status = useAppSelector((state) => state.groups.createGroup.status); const dispatch = useAppDispatch(); useEffect(() => { diff --git a/src/views/home/groups/ModalInvite.tsx b/src/views/home/groups/ModalInvite.tsx new file mode 100644 index 0000000..a00cfec --- /dev/null +++ b/src/views/home/groups/ModalInvite.tsx @@ -0,0 +1,102 @@ +import { FC, useEffect, useMemo } from 'react'; +import { useAppDispatch, useAppSelector } from '../../../redux/hooks'; +import { fetchGroupJoinLink } from '../../../redux/slices/groups'; +import { Modal } from '../../../components/modal/Modal'; +import { PrimaryButton } from '../../../components/button/PrimaryButton'; +import { SecondaryButton } from '../../../components/button/SecondaryButton'; +import { Input } from '../../../components/input/Input'; + +interface ModalInviteProps { + active: boolean; + setActive: (value: boolean) => void; + groupId: number; + groupName: string; +} + +const ModalInvite: FC = ({ + active, + setActive, + groupId, + groupName, +}) => { + const dispatch = useAppDispatch(); + const baseUrl = window.location.origin; + + // Получаем токен и дату из Redux + const { joinLink, status } = useAppSelector( + (state) => state.groups.fetchGroupJoinLink, + ); + + // При открытии модалки запрашиваем join link + useEffect(() => { + if (active) { + dispatch(fetchGroupJoinLink(groupId)); + } + }, [active, groupId, dispatch]); + + // Генерация полной ссылки с query параметрами + const inviteLink = useMemo(() => { + if (!joinLink) return ''; + const params = new URLSearchParams({ + token: joinLink.token, + expiresAt: joinLink.expiresAt, + groupName, + groupId: `${groupId}`, + }); + return `${baseUrl}/home/group-invite?${params.toString()}`; + }, [joinLink, groupName, baseUrl, groupId]); + + // Копирование и закрытие модалки + const handleCopy = async () => { + if (!inviteLink) return; + try { + await navigator.clipboard.writeText(inviteLink); + setActive(false); + } catch (err) { + console.error('Не удалось скопировать ссылку:', err); + } + }; + + return ( + +
+
+ Приглашение в группу "{groupName}" +
+ +
+
+ Ссылка для приглашения +
+
+ {inviteLink} +
+
+ +
+ + setActive(false)} + text="Отмена" + /> +
+
+
+ ); +}; + +export default ModalInvite; diff --git a/src/views/home/groups/ModalUpdate.tsx b/src/views/home/groups/ModalUpdate.tsx index 9233c9f..5d4ec0a 100644 --- a/src/views/home/groups/ModalUpdate.tsx +++ b/src/views/home/groups/ModalUpdate.tsx @@ -24,10 +24,10 @@ const ModalUpdate: FC = ({ const [name, setName] = useState(''); const [description, setDescription] = useState(''); const statusUpdate = useAppSelector( - (state) => state.groups.statuses.update, + (state) => state.groups.updateGroup.status, ); const statusDelete = useAppSelector( - (state) => state.groups.statuses.delete, + (state) => state.groups.deleteGroup.status, ); const dispatch = useAppDispatch(); diff --git a/src/views/home/menu/Menu.tsx b/src/views/home/menu/Menu.tsx index e7b6ce4..3329c04 100644 --- a/src/views/home/menu/Menu.tsx +++ b/src/views/home/menu/Menu.tsx @@ -1,4 +1,4 @@ -import { Logo } from '../../../assets/logos'; +import { Logo, LogoFASIE } from '../../../assets/logos'; import { Account, Clipboard, @@ -42,7 +42,7 @@ const Menu = () => { const activePage = useAppSelector((state) => state.store.menu.activePage); return ( -
+
{menuItems.map((v, i) => ( @@ -56,6 +56,15 @@ const Menu = () => { /> ))}
+ +
+ +
+ { + 'Проект «LiquidCode» создан при поддержке Федерального государственного бюджетного учреждения «Фонд содействия развитию малых форм предприятий в научно-технической сфере» в рамках программы «Студенческий стартап» федерального проекта «Платформа университетского технологического предпринимательства»' + } +
+
); }; diff --git a/src/views/home/missions/Filter.tsx b/src/views/home/missions/Filter.tsx new file mode 100644 index 0000000..ca01a9d --- /dev/null +++ b/src/views/home/missions/Filter.tsx @@ -0,0 +1,51 @@ +import { + FilterDropDown, + FilterItem, +} from '../../../components/drop-down-list/Filter'; +import { SorterDropDown } from '../../../components/drop-down-list/Sorter'; +import { SearchInput } from '../../../components/input/SearchInput'; + +const Filters = () => { + const items: FilterItem[] = [ + { text: 'React', value: 'react' }, + { text: 'Vue', value: 'vue' }, + { text: 'Angular', value: 'angular' }, + { text: 'Svelte', value: 'svelte' }, + { text: 'Next.js', value: 'next' }, + { text: 'Nuxt', value: 'nuxt' }, + { text: 'Solid', value: 'solid' }, + { text: 'Qwik', value: 'qwik' }, + ]; + + return ( +
+ {}} placeholder="Поиск задачи" /> + + console.log(v)} + /> + + console.log(values)} + /> +
+ ); +}; + +export default Filters; diff --git a/src/views/home/missions/Missions.tsx b/src/views/home/missions/Missions.tsx index 3b5203e..28d71b0 100644 --- a/src/views/home/missions/Missions.tsx +++ b/src/views/home/missions/Missions.tsx @@ -5,6 +5,7 @@ import { useEffect, useState } from 'react'; import { setMenuActivePage } from '../../../redux/slices/store'; import { fetchMissions } from '../../../redux/slices/missions'; import ModalCreate from './ModalCreate'; +import Filters from './Filter'; export interface Mission { id: number; @@ -45,7 +46,7 @@ const Missions = () => { />
-
+
{missions.map((v, i) => ( diff --git a/src/views/home/missions/ModalCreate.tsx b/src/views/home/missions/ModalCreate.tsx index e93fce6..c36fa3a 100644 --- a/src/views/home/missions/ModalCreate.tsx +++ b/src/views/home/missions/ModalCreate.tsx @@ -97,7 +97,7 @@ const ModalCreate: FC = ({ active, setActive }) => {
-