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 (
+
+ {
+ setValue(e.target.value);
+ }}
+ onKeyDown={(e: React.KeyboardEvent) => {
+ if (onKeyDown) onKeyDown(e);
+ }}
+ />
+
+
+ );
+};
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="Новая статья"
/>
- {/* Блок для тегов */}
+ {/* Теги */}
+ {/* Просмотр и переход в редактор */}
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 (
+
+ );
+};
+
+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)}
>
Мои статьи
+
+ {/* Контент */}
- {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 }) => {
Файл задачи
-
+
{file ? file.name : 'Выбрать файл'}
{
+ const items = [
+ {
+ name: 'Энтузиаст создал карточки с NFC-метками для знакомства ребёнка с музыкой',
+ },
+ {
+ name: 'Алгоритм Древа Силы, Космический Сортировщик',
+ },
+ {
+ name: 'Космический Сортировщик',
+ },
+ {
+ name: 'Зеркала Многомерности',
+ },
+ ];
+ return (
+
+
+ Попоулярные статьи
+
+
+ {items.map((v, i) => {
+ return (
+ <>
+ {
+
+ {v.name}
+
+ }
+ {i + 1 != items.length && (
+
+ )}
+ >
+ );
+ })}
+
+ );
+};
diff --git a/src/views/home/rightpanel/Group.tsx b/src/views/home/rightpanel/Group.tsx
new file mode 100644
index 0000000..28b26fb
--- /dev/null
+++ b/src/views/home/rightpanel/Group.tsx
@@ -0,0 +1,60 @@
+import { FC } from 'react';
+
+export const GroupRightPanel: FC = () => {
+ const items = [
+ {
+ name: 'Игнат Герасименко',
+ role: 'Администратор',
+ },
+ {
+ name: 'Алиса Макаренко',
+ role: 'Модератор',
+ },
+ {
+ name: 'Федор Картман',
+ role: 'Модератор',
+ },
+ {
+ name: 'Карина Механаджанович',
+ role: 'Участник',
+ },
+ {
+ name: 'Михаил Ангрский',
+ role: 'Участник',
+ },
+ {
+ name: 'newuser',
+ role: 'Участник (Вы)',
+ },
+ ];
+ return (
+
+
+ Пользователи
+
+
+ {items.map((v, i) => {
+ return (
+ <>
+ {
+
+
+
+
+ {v.name}
+
+
+ {v.role}
+
+
+
+ }
+ {i + 1 != items.length && (
+
+ )}
+ >
+ );
+ })}
+
+ );
+};
diff --git a/src/views/home/rightpanel/Missions.tsx b/src/views/home/rightpanel/Missions.tsx
new file mode 100644
index 0000000..2d2abe8
--- /dev/null
+++ b/src/views/home/rightpanel/Missions.tsx
@@ -0,0 +1,68 @@
+import { FC } from 'react';
+import { cn } from '../../../lib/cn';
+
+export const MissionsRightPanel: FC = () => {
+ const items = [
+ {
+ name: 'Кромсатели металла v4',
+ difficulty: 'Easy',
+ tags: ['strings', 'arrays', 'math'],
+ },
+ {
+ name: 'Алгоритм Древа Силы',
+ difficulty: 'Medium',
+ tags: ['trees', 'dfs', 'recursion'],
+ },
+ {
+ name: 'Космический Сортировщик',
+ difficulty: 'Hard',
+ tags: ['sorting', 'optimization', 'greedy'],
+ },
+ {
+ name: 'Зеркала Многомерности',
+ difficulty: 'Medium',
+ tags: ['matrix', 'geometry', 'simulation'],
+ },
+ ];
+ return (
+
+
+ Новые задачи
+
+
+ {items.map((v, i) => {
+ return (
+ <>
+ {
+
+
{v.name}
+
+ {v.difficulty}
+
+
+ {v.tags.slice(0, 2).map((v, i) => (
+
{v}
+ ))}
+ {v.tags.length > 2 && '...'}
+
+
+ }
+ {i + 1 != items.length && (
+
+ )}
+ >
+ );
+ })}
+
+ );
+};
diff --git a/src/views/mission/statement/MissionSubmissions.tsx b/src/views/mission/statement/MissionSubmissions.tsx
index cdd725d..04dca84 100644
--- a/src/views/mission/statement/MissionSubmissions.tsx
+++ b/src/views/mission/statement/MissionSubmissions.tsx
@@ -1,6 +1,6 @@
import SubmissionItem from './SubmissionItem';
import { useAppSelector } from '../../../redux/hooks';
-import { FC, useEffect } from 'react';
+import { FC } from 'react';
export interface Mission {
id: number;
@@ -16,45 +16,46 @@ export interface Mission {
interface MissionSubmissionsProps {
missionId: number;
+ contestId?: number;
}
-const MissionSubmissions: FC = ({ missionId }) => {
+const MissionSubmissions: FC = ({ missionId, contestId }) => {
const submissions = useAppSelector(
- (state) => state.submin.submitsById[missionId],
+ (state) => state.submin.submitsById[missionId] || []
);
- useEffect(() => {}, []);
-
const checkStatus = (status: string) => {
- if (status == 'IncorrectAnswer') return 'wronganswer';
- if (status == 'TimeLimitError') return 'timelimit';
+ if (status === 'IncorrectAnswer') return 'wronganswer';
+ if (status === 'TimeLimitError') return 'timelimit';
return undefined;
};
+ // Если contestId передан, фильтруем по нему, иначе показываем все
+ const filteredSubmissions = contestId
+ ? submissions.filter((v) => v.contestId === contestId)
+ : submissions;
+
return (
-
- {submissions &&
- submissions.map((v, i) => (
-
- ))}
+
+ {filteredSubmissions.map((v, i) => (
+
+ ))}
);
};
diff --git a/tsconfig.app.tsbuildinfo b/tsconfig.app.tsbuildinfo
index dc32943..f725851 100644
--- a/tsconfig.app.tsbuildinfo
+++ b/tsconfig.app.tsbuildinfo
@@ -1 +1 @@
-{"root":["./src/app.tsx","./src/axios.ts","./src/main.tsx","./src/vite-env.d.ts","./src/assets/icons/account/index.ts","./src/assets/icons/auth/index.ts","./src/assets/icons/groups/index.ts","./src/assets/icons/header/index.ts","./src/assets/icons/input/index.ts","./src/assets/icons/menu/index.ts","./src/assets/icons/missions/index.ts","./src/assets/logos/index.ts","./src/components/button/primarybutton.tsx","./src/components/button/reversebutton.tsx","./src/components/button/secondarybutton.tsx","./src/components/checkbox/checkbox.tsx","./src/components/drop-down-list/dropdownlist.tsx","./src/components/input/daterangeinput.tsx","./src/components/input/input.tsx","./src/components/modal/modal.tsx","./src/components/router/protectedroute.tsx","./src/components/switch/switch.tsx","./src/config/colors.ts","./src/hooks/useclickoutside.ts","./src/hooks/usequery.ts","./src/lib/cn.ts","./src/pages/article.tsx","./src/pages/articleeditor.tsx","./src/pages/home.tsx","./src/pages/mission.tsx","./src/redux/hooks.ts","./src/redux/store.ts","./src/redux/slices/account.ts","./src/redux/slices/articles.ts","./src/redux/slices/auth.ts","./src/redux/slices/contests.ts","./src/redux/slices/groups.ts","./src/redux/slices/missions.ts","./src/redux/slices/store.ts","./src/redux/slices/submit.ts","./src/views/article/header.tsx","./src/views/articleeditor/editor.tsx","./src/views/articleeditor/header.tsx","./src/views/articleeditor/marckdownpreview.tsx","./src/views/home/account/account.tsx","./src/views/home/account/accoutmenu.tsx","./src/views/home/account/articlesblock.tsx","./src/views/home/account/contestsblock.tsx","./src/views/home/account/missionsblock.tsx","./src/views/home/account/rightpanel.tsx","./src/views/home/articles/articleitem.tsx","./src/views/home/articles/articles.tsx","./src/views/home/auth/login.tsx","./src/views/home/auth/register.tsx","./src/views/home/contest/contest.tsx","./src/views/home/contest/missionitem.tsx","./src/views/home/contest/missions.tsx","./src/views/home/contest/submissions.tsx","./src/views/home/contests/contestitem.tsx","./src/views/home/contests/contests.tsx","./src/views/home/contests/contestsblock.tsx","./src/views/home/contests/modalcreate.tsx","./src/views/home/groups/group.tsx","./src/views/home/groups/groupitem.tsx","./src/views/home/groups/groups.tsx","./src/views/home/groups/groupsblock.tsx","./src/views/home/groups/modalcreate.tsx","./src/views/home/groups/modalupdate.tsx","./src/views/home/menu/menu.tsx","./src/views/home/menu/menuitem.tsx","./src/views/home/missions/missionitem.tsx","./src/views/home/missions/missions.tsx","./src/views/home/missions/modalcreate.tsx","./src/views/mission/codeeditor/codeeditor.tsx","./src/views/mission/statement/header.tsx","./src/views/mission/statement/latextcontainer.tsx","./src/views/mission/statement/missionsubmissions.tsx","./src/views/mission/statement/statement.tsx","./src/views/mission/statement/submissionitem.tsx","./src/views/mission/submission/submission.tsx"],"version":"5.6.2"}
\ No newline at end of file
+{"root":["./src/app.tsx","./src/axios.ts","./src/main.tsx","./src/vite-env.d.ts","./src/assets/icons/account/index.ts","./src/assets/icons/auth/index.ts","./src/assets/icons/filters/index.ts","./src/assets/icons/groups/index.ts","./src/assets/icons/header/index.ts","./src/assets/icons/input/index.ts","./src/assets/icons/menu/index.ts","./src/assets/icons/missions/index.ts","./src/assets/logos/index.ts","./src/components/button/primarybutton.tsx","./src/components/button/reversebutton.tsx","./src/components/button/secondarybutton.tsx","./src/components/checkbox/checkbox.tsx","./src/components/drop-down-list/dropdownlist.tsx","./src/components/drop-down-list/filter.tsx","./src/components/drop-down-list/sorter.tsx","./src/components/input/daterangeinput.tsx","./src/components/input/input.tsx","./src/components/input/searchinput.tsx","./src/components/modal/modal.tsx","./src/components/router/protectedroute.tsx","./src/components/switch/switch.tsx","./src/config/colors.ts","./src/hooks/useclickoutside.ts","./src/hooks/usequery.ts","./src/lib/cn.ts","./src/pages/article.tsx","./src/pages/articleeditor.tsx","./src/pages/contesteditor.tsx","./src/pages/home.tsx","./src/pages/mission.tsx","./src/redux/hooks.ts","./src/redux/store.ts","./src/redux/slices/articles.ts","./src/redux/slices/auth.ts","./src/redux/slices/contests.ts","./src/redux/slices/groups.ts","./src/redux/slices/missions.ts","./src/redux/slices/store.ts","./src/redux/slices/submit.ts","./src/views/article/header.tsx","./src/views/articleeditor/editor.tsx","./src/views/articleeditor/header.tsx","./src/views/articleeditor/marckdownpreview.tsx","./src/views/home/account/account.tsx","./src/views/home/account/accoutmenu.tsx","./src/views/home/account/rightpanel.tsx","./src/views/home/account/articles/articlesblock.tsx","./src/views/home/account/contests/contests.tsx","./src/views/home/account/contests/contestsblock.tsx","./src/views/home/account/contests/mycontestitem.tsx","./src/views/home/account/contests/registercontestitem.tsx","./src/views/home/account/missions/missions.tsx","./src/views/home/account/missions/missionsblock.tsx","./src/views/home/account/missions/mymissionitem.tsx","./src/views/home/articles/articleitem.tsx","./src/views/home/articles/articles.tsx","./src/views/home/articles/filter.tsx","./src/views/home/auth/login.tsx","./src/views/home/auth/register.tsx","./src/views/home/contest/contest.tsx","./src/views/home/contest/missionitem.tsx","./src/views/home/contest/missions.tsx","./src/views/home/contest/submissionitem.tsx","./src/views/home/contest/submissions.tsx","./src/views/home/contests/contestitem.tsx","./src/views/home/contests/contests.tsx","./src/views/home/contests/contestsblock.tsx","./src/views/home/contests/filter.tsx","./src/views/home/contests/modalcreate.tsx","./src/views/home/groups/filter.tsx","./src/views/home/groups/group.tsx","./src/views/home/groups/groupitem.tsx","./src/views/home/groups/groups.tsx","./src/views/home/groups/groupsblock.tsx","./src/views/home/groups/modalcreate.tsx","./src/views/home/groups/modalupdate.tsx","./src/views/home/menu/menu.tsx","./src/views/home/menu/menuitem.tsx","./src/views/home/missions/filter.tsx","./src/views/home/missions/missionitem.tsx","./src/views/home/missions/missions.tsx","./src/views/home/missions/modalcreate.tsx","./src/views/mission/codeeditor/codeeditor.tsx","./src/views/mission/statement/header.tsx","./src/views/mission/statement/latextcontainer.tsx","./src/views/mission/statement/missionsubmissions.tsx","./src/views/mission/statement/statement.tsx","./src/views/mission/statement/submissionitem.tsx","./src/views/mission/submission/submission.tsx"],"version":"5.6.2"}
\ No newline at end of file