diff --git a/package-lock.json b/package-lock.json
index 799d483..ec3a875 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,16 +10,23 @@
"dependencies": {
"@monaco-editor/react": "^4.7.0",
"@reduxjs/toolkit": "^2.9.2",
+ "@tailwindcss/typography": "^0.5.19",
"@types/react-redux": "^7.1.33",
"axios": "^1.12.2",
"clsx": "^2.1.1",
"framer-motion": "^11.9.0",
+ "highlight.js": "^11.11.1",
"monaco-editor": "^0.54.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",
+ "rehype-highlight": "^7.0.2",
+ "rehype-raw": "^7.0.0",
+ "rehype-sanitize": "^6.0.0",
+ "remark-gfm": "^4.0.1",
"tailwind-cn": "^1.0.2",
"tailwind-merge": "^1.14.0",
"tailwindcss": "^3.4.12"
@@ -1354,6 +1361,31 @@
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
"license": "MIT"
},
+ "node_modules/@tailwindcss/typography": {
+ "version": "0.5.19",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.19.tgz",
+ "integrity": "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==",
+ "license": "MIT",
+ "dependencies": {
+ "postcss-selector-parser": "6.0.10"
+ },
+ "peerDependencies": {
+ "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1"
+ }
+ },
+ "node_modules/@tailwindcss/typography/node_modules/postcss-selector-parser": {
+ "version": "6.0.10",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
+ "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==",
+ "license": "MIT",
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/@types/babel__core": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@@ -1399,13 +1431,39 @@
"@babel/types": "^7.20.7"
}
},
+ "node_modules/@types/debug": {
+ "version": "4.1.12",
+ "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
+ "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/ms": "*"
+ }
+ },
"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==",
- "dev": true,
"license": "MIT"
},
+ "node_modules/@types/estree-jsx": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz",
+ "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "*"
+ }
+ },
+ "node_modules/@types/hast": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
+ "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "*"
+ }
+ },
"node_modules/@types/hoist-non-react-statics": {
"version": "3.3.5",
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz",
@@ -1416,6 +1474,21 @@
"hoist-non-react-statics": "^3.3.0"
}
},
+ "node_modules/@types/mdast": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
+ "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "*"
+ }
+ },
+ "node_modules/@types/ms": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
+ "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
+ "license": "MIT"
+ },
"node_modules/@types/prop-types": {
"version": "15.7.13",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz",
@@ -1463,6 +1536,12 @@
"@babel/runtime": "^7.9.2"
}
},
+ "node_modules/@types/unist": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
+ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
+ "license": "MIT"
+ },
"node_modules/@types/use-sync-external-store": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
@@ -1711,6 +1790,12 @@
"url": "https://opencollective.com/eslint"
}
},
+ "node_modules/@ungap/structured-clone": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
+ "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
+ "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",
@@ -1880,6 +1965,16 @@
"proxy-from-env": "^1.1.0"
}
},
+ "node_modules/bail": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
+ "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -2007,6 +2102,16 @@
],
"license": "CC-BY-4.0"
},
+ "node_modules/ccount": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz",
+ "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
@@ -2022,6 +2127,46 @@
"node": ">=4"
}
},
+ "node_modules/character-entities": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz",
+ "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/character-entities-html4": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz",
+ "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/character-entities-legacy": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz",
+ "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/character-reference-invalid": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz",
+ "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
@@ -2096,6 +2241,16 @@
"node": ">= 0.8"
}
},
+ "node_modules/comma-separated-tokens": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
+ "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/commander": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
@@ -2164,7 +2319,6 @@
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
@@ -2178,6 +2332,19 @@
}
}
},
+ "node_modules/decode-named-character-reference": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz",
+ "integrity": "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==",
+ "license": "MIT",
+ "dependencies": {
+ "character-entities": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/deep-is": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
@@ -2194,6 +2361,28 @@
"node": ">=0.4.0"
}
},
+ "node_modules/dequal": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
+ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/devlop": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
+ "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==",
+ "license": "MIT",
+ "dependencies": {
+ "dequal": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/didyoumean": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
@@ -2245,6 +2434,18 @@
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
"license": "MIT"
},
+ "node_modules/entities": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
+ "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
@@ -2605,6 +2806,16 @@
"node": ">=4.0"
}
},
+ "node_modules/estree-util-is-identifier-name": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz",
+ "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/esutils": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
@@ -2615,6 +2826,12 @@
"node": ">=0.10.0"
}
},
+ "node_modules/extend": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
+ "license": "MIT"
+ },
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -3034,6 +3251,203 @@
"node": ">= 0.4"
}
},
+ "node_modules/hast-util-from-parse5": {
+ "version": "8.0.3",
+ "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz",
+ "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/unist": "^3.0.0",
+ "devlop": "^1.0.0",
+ "hastscript": "^9.0.0",
+ "property-information": "^7.0.0",
+ "vfile": "^6.0.0",
+ "vfile-location": "^5.0.0",
+ "web-namespaces": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hast-util-is-element": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz",
+ "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hast-util-parse-selector": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz",
+ "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hast-util-raw": {
+ "version": "9.1.0",
+ "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.1.0.tgz",
+ "integrity": "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/unist": "^3.0.0",
+ "@ungap/structured-clone": "^1.0.0",
+ "hast-util-from-parse5": "^8.0.0",
+ "hast-util-to-parse5": "^8.0.0",
+ "html-void-elements": "^3.0.0",
+ "mdast-util-to-hast": "^13.0.0",
+ "parse5": "^7.0.0",
+ "unist-util-position": "^5.0.0",
+ "unist-util-visit": "^5.0.0",
+ "vfile": "^6.0.0",
+ "web-namespaces": "^2.0.0",
+ "zwitch": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hast-util-sanitize": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/hast-util-sanitize/-/hast-util-sanitize-5.0.2.tgz",
+ "integrity": "sha512-3yTWghByc50aGS7JlGhk61SPenfE/p1oaFeNwkOOyrscaOkMGrcW9+Cy/QAIOBpZxP1yqDIzFMR0+Np0i0+usg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@ungap/structured-clone": "^1.0.0",
+ "unist-util-position": "^5.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hast-util-to-jsx-runtime": {
+ "version": "2.3.6",
+ "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz",
+ "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0",
+ "@types/hast": "^3.0.0",
+ "@types/unist": "^3.0.0",
+ "comma-separated-tokens": "^2.0.0",
+ "devlop": "^1.0.0",
+ "estree-util-is-identifier-name": "^3.0.0",
+ "hast-util-whitespace": "^3.0.0",
+ "mdast-util-mdx-expression": "^2.0.0",
+ "mdast-util-mdx-jsx": "^3.0.0",
+ "mdast-util-mdxjs-esm": "^2.0.0",
+ "property-information": "^7.0.0",
+ "space-separated-tokens": "^2.0.0",
+ "style-to-js": "^1.0.0",
+ "unist-util-position": "^5.0.0",
+ "vfile-message": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hast-util-to-parse5": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.0.tgz",
+ "integrity": "sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "comma-separated-tokens": "^2.0.0",
+ "devlop": "^1.0.0",
+ "property-information": "^6.0.0",
+ "space-separated-tokens": "^2.0.0",
+ "web-namespaces": "^2.0.0",
+ "zwitch": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hast-util-to-parse5/node_modules/property-information": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz",
+ "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/hast-util-to-text": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz",
+ "integrity": "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/unist": "^3.0.0",
+ "hast-util-is-element": "^3.0.0",
+ "unist-util-find-after": "^5.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hast-util-whitespace": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz",
+ "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hastscript": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz",
+ "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "comma-separated-tokens": "^2.0.0",
+ "hast-util-parse-selector": "^4.0.0",
+ "property-information": "^7.0.0",
+ "space-separated-tokens": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/highlight.js": {
+ "version": "11.11.1",
+ "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz",
+ "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
"node_modules/hoist-non-react-statics": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
@@ -3043,6 +3457,26 @@
"react-is": "^16.7.0"
}
},
+ "node_modules/html-url-attributes": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz",
+ "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/html-void-elements": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz",
+ "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -3090,6 +3524,36 @@
"node": ">=0.8.19"
}
},
+ "node_modules/inline-style-parser": {
+ "version": "0.2.6",
+ "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.6.tgz",
+ "integrity": "sha512-gtGXVaBdl5mAes3rPcMedEBm12ibjt1kDMFfheul1wUAOVEJW60voNdMVzVkfLN06O7ZaD/rxhfKgtlgtTbMjg==",
+ "license": "MIT"
+ },
+ "node_modules/is-alphabetical": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz",
+ "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/is-alphanumerical": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz",
+ "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==",
+ "license": "MIT",
+ "dependencies": {
+ "is-alphabetical": "^2.0.0",
+ "is-decimal": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
@@ -3117,6 +3581,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-decimal": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz",
+ "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -3147,6 +3621,16 @@
"node": ">=0.10.0"
}
},
+ "node_modules/is-hexadecimal": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz",
+ "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
@@ -3166,6 +3650,18 @@
"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",
+ "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -3324,6 +3820,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/longest-streak": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz",
+ "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
@@ -3336,6 +3842,21 @@
"loose-envify": "cli.js"
}
},
+ "node_modules/lowlight": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-3.3.0.tgz",
+ "integrity": "sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "devlop": "^1.0.0",
+ "highlight.js": "~11.11.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
@@ -3346,6 +3867,16 @@
"yallist": "^3.0.2"
}
},
+ "node_modules/markdown-table": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz",
+ "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/marked": {
"version": "14.0.0",
"resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz",
@@ -3367,6 +3898,288 @@
"node": ">= 0.4"
}
},
+ "node_modules/mdast-util-find-and-replace": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz",
+ "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "escape-string-regexp": "^5.0.0",
+ "unist-util-is": "^6.0.0",
+ "unist-util-visit-parents": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
+ "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/mdast-util-from-markdown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz",
+ "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "@types/unist": "^3.0.0",
+ "decode-named-character-reference": "^1.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-to-string": "^4.0.0",
+ "micromark": "^4.0.0",
+ "micromark-util-decode-numeric-character-reference": "^2.0.0",
+ "micromark-util-decode-string": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0",
+ "unist-util-stringify-position": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-gfm": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz",
+ "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==",
+ "license": "MIT",
+ "dependencies": {
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-gfm-autolink-literal": "^2.0.0",
+ "mdast-util-gfm-footnote": "^2.0.0",
+ "mdast-util-gfm-strikethrough": "^2.0.0",
+ "mdast-util-gfm-table": "^2.0.0",
+ "mdast-util-gfm-task-list-item": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-gfm-autolink-literal": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz",
+ "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "ccount": "^2.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-find-and-replace": "^3.0.0",
+ "micromark-util-character": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-gfm-footnote": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz",
+ "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.1.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-gfm-strikethrough": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz",
+ "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-gfm-table": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz",
+ "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "markdown-table": "^3.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-gfm-task-list-item": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz",
+ "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-mdx-expression": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz",
+ "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree-jsx": "^1.0.0",
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-mdx-jsx": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz",
+ "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree-jsx": "^1.0.0",
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "@types/unist": "^3.0.0",
+ "ccount": "^2.0.0",
+ "devlop": "^1.1.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0",
+ "parse-entities": "^4.0.0",
+ "stringify-entities": "^4.0.0",
+ "unist-util-stringify-position": "^4.0.0",
+ "vfile-message": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-mdxjs-esm": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz",
+ "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree-jsx": "^1.0.0",
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-phrasing": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz",
+ "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "unist-util-is": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-to-hast": {
+ "version": "13.2.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz",
+ "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "@ungap/structured-clone": "^1.0.0",
+ "devlop": "^1.0.0",
+ "micromark-util-sanitize-uri": "^2.0.0",
+ "trim-lines": "^3.0.0",
+ "unist-util-position": "^5.0.0",
+ "unist-util-visit": "^5.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-to-markdown": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz",
+ "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "@types/unist": "^3.0.0",
+ "longest-streak": "^3.0.0",
+ "mdast-util-phrasing": "^4.0.0",
+ "mdast-util-to-string": "^4.0.0",
+ "micromark-util-classify-character": "^2.0.0",
+ "micromark-util-decode-string": "^2.0.0",
+ "unist-util-visit": "^5.0.0",
+ "zwitch": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-to-string": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz",
+ "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@@ -3376,6 +4189,569 @@
"node": ">= 8"
}
},
+ "node_modules/micromark": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz",
+ "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@types/debug": "^4.0.0",
+ "debug": "^4.0.0",
+ "decode-named-character-reference": "^1.0.0",
+ "devlop": "^1.0.0",
+ "micromark-core-commonmark": "^2.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-combine-extensions": "^2.0.0",
+ "micromark-util-decode-numeric-character-reference": "^2.0.0",
+ "micromark-util-encode": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0",
+ "micromark-util-resolve-all": "^2.0.0",
+ "micromark-util-sanitize-uri": "^2.0.0",
+ "micromark-util-subtokenize": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-core-commonmark": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz",
+ "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "decode-named-character-reference": "^1.0.0",
+ "devlop": "^1.0.0",
+ "micromark-factory-destination": "^2.0.0",
+ "micromark-factory-label": "^2.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-factory-title": "^2.0.0",
+ "micromark-factory-whitespace": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-classify-character": "^2.0.0",
+ "micromark-util-html-tag-name": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0",
+ "micromark-util-resolve-all": "^2.0.0",
+ "micromark-util-subtokenize": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-extension-gfm": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz",
+ "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==",
+ "license": "MIT",
+ "dependencies": {
+ "micromark-extension-gfm-autolink-literal": "^2.0.0",
+ "micromark-extension-gfm-footnote": "^2.0.0",
+ "micromark-extension-gfm-strikethrough": "^2.0.0",
+ "micromark-extension-gfm-table": "^2.0.0",
+ "micromark-extension-gfm-tagfilter": "^2.0.0",
+ "micromark-extension-gfm-task-list-item": "^2.0.0",
+ "micromark-util-combine-extensions": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm-autolink-literal": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz",
+ "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==",
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-sanitize-uri": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm-footnote": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz",
+ "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==",
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-core-commonmark": "^2.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0",
+ "micromark-util-sanitize-uri": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm-strikethrough": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz",
+ "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==",
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-classify-character": "^2.0.0",
+ "micromark-util-resolve-all": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm-table": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz",
+ "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==",
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm-tagfilter": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz",
+ "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==",
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm-task-list-item": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz",
+ "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==",
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-factory-destination": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz",
+ "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-label": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz",
+ "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-space": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz",
+ "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-title": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz",
+ "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-whitespace": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz",
+ "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-character": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz",
+ "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-chunked": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz",
+ "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-classify-character": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz",
+ "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-combine-extensions": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz",
+ "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-decode-numeric-character-reference": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz",
+ "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-decode-string": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz",
+ "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "decode-named-character-reference": "^1.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-decode-numeric-character-reference": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-encode": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz",
+ "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/micromark-util-html-tag-name": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz",
+ "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/micromark-util-normalize-identifier": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz",
+ "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-resolve-all": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz",
+ "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-sanitize-uri": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz",
+ "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-encode": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-subtokenize": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz",
+ "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-symbol": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz",
+ "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/micromark-util-types": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz",
+ "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
+ },
"node_modules/micromatch": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
@@ -3446,7 +4822,6 @@
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "dev": true,
"license": "MIT"
},
"node_modules/mz": {
@@ -3598,6 +4973,43 @@
"node": ">=6"
}
},
+ "node_modules/parse-entities": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz",
+ "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^2.0.0",
+ "character-entities-legacy": "^3.0.0",
+ "character-reference-invalid": "^2.0.0",
+ "decode-named-character-reference": "^1.0.0",
+ "is-alphanumerical": "^2.0.0",
+ "is-decimal": "^2.0.0",
+ "is-hexadecimal": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/parse-entities/node_modules/@types/unist": {
+ "version": "2.0.11",
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz",
+ "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
+ "license": "MIT"
+ },
+ "node_modules/parse5": {
+ "version": "7.3.0",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
+ "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
+ "license": "MIT",
+ "dependencies": {
+ "entities": "^6.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -3846,6 +5258,16 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/property-information": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz",
+ "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
@@ -3913,6 +5335,33 @@
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"license": "MIT"
},
+ "node_modules/react-markdown": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz",
+ "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "hast-util-to-jsx-runtime": "^2.0.0",
+ "html-url-attributes": "^3.0.0",
+ "mdast-util-to-hast": "^13.0.0",
+ "remark-parse": "^11.0.0",
+ "remark-rehype": "^11.0.0",
+ "unified": "^11.0.0",
+ "unist-util-visit": "^5.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ },
+ "peerDependencies": {
+ "@types/react": ">=18",
+ "react": ">=18"
+ }
+ },
"node_modules/react-redux": {
"version": "9.2.0",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
@@ -4026,6 +5475,118 @@
"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",
+ "integrity": "sha512-k158pK7wdC2qL3M5NcZROZ2tR/l7zOzjxXd5VGdcfIyoijjQqpHd3JKtYSBDpDZ38UI2WJWuFAtkMDxmx5kstA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "hast-util-to-text": "^4.0.0",
+ "lowlight": "^3.0.0",
+ "unist-util-visit": "^5.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/rehype-raw": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz",
+ "integrity": "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "hast-util-raw": "^9.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/rehype-sanitize": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/rehype-sanitize/-/rehype-sanitize-6.0.0.tgz",
+ "integrity": "sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "hast-util-sanitize": "^5.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/remark-gfm": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz",
+ "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "mdast-util-gfm": "^3.0.0",
+ "micromark-extension-gfm": "^3.0.0",
+ "remark-parse": "^11.0.0",
+ "remark-stringify": "^11.0.0",
+ "unified": "^11.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/remark-parse": {
+ "version": "11.0.0",
+ "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz",
+ "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "micromark-util-types": "^2.0.0",
+ "unified": "^11.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/remark-rehype": {
+ "version": "11.1.2",
+ "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz",
+ "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "mdast-util-to-hast": "^13.0.0",
+ "unified": "^11.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/remark-stringify": {
+ "version": "11.0.0",
+ "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz",
+ "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "mdast-util-to-markdown": "^2.0.0",
+ "unified": "^11.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/reselect": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
@@ -4195,6 +5756,16 @@
"node": ">=0.10.0"
}
},
+ "node_modules/space-separated-tokens": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz",
+ "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/state-local": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz",
@@ -4266,6 +5837,20 @@
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
+ "node_modules/stringify-entities": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
+ "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==",
+ "license": "MIT",
+ "dependencies": {
+ "character-entities-html4": "^2.0.0",
+ "character-entities-legacy": "^3.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
@@ -4304,6 +5889,24 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/style-to-js": {
+ "version": "1.1.19",
+ "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.19.tgz",
+ "integrity": "sha512-Ev+SgeqiNGT1ufsXyVC5RrJRXdrkRJ1Gol9Qw7Pb72YCKJXrBvP0ckZhBeVSrw2m06DJpei2528uIpjMb4TsoQ==",
+ "license": "MIT",
+ "dependencies": {
+ "style-to-object": "1.0.12"
+ }
+ },
+ "node_modules/style-to-object": {
+ "version": "1.0.12",
+ "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.12.tgz",
+ "integrity": "sha512-ddJqYnoT4t97QvN2C95bCgt+m7AAgXjVnkk/jxAfmp7EAB8nnqqZYEbMd3em7/vEomDb2LAQKAy1RFfv41mdNw==",
+ "license": "MIT",
+ "dependencies": {
+ "inline-style-parser": "0.2.6"
+ }
+ },
"node_modules/sucrase": {
"version": "3.35.0",
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
@@ -4458,6 +6061,26 @@
"node": ">=8.0"
}
},
+ "node_modules/trim-lines": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
+ "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/trough": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz",
+ "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/ts-api-utils": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz",
@@ -4534,6 +6157,107 @@
}
}
},
+ "node_modules/unified": {
+ "version": "11.0.5",
+ "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz",
+ "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "bail": "^2.0.0",
+ "devlop": "^1.0.0",
+ "extend": "^3.0.0",
+ "is-plain-obj": "^4.0.0",
+ "trough": "^2.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-find-after": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz",
+ "integrity": "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "unist-util-is": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-is": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz",
+ "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-position": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz",
+ "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-stringify-position": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz",
+ "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-visit": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz",
+ "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "unist-util-is": "^6.0.0",
+ "unist-util-visit-parents": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-visit-parents": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz",
+ "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "unist-util-is": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/update-browserslist-db": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz",
@@ -4590,6 +6314,48 @@
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
+ "node_modules/vfile": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
+ "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "vfile-message": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/vfile-location": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz",
+ "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/vfile-message": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz",
+ "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "unist-util-stringify-position": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/vite": {
"version": "5.4.6",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.6.tgz",
@@ -4650,6 +6416,16 @@
}
}
},
+ "node_modules/web-namespaces": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz",
+ "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -4833,6 +6609,16 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
+ },
+ "node_modules/zwitch": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
+ "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
}
}
}
diff --git a/package.json b/package.json
index 8587ab6..946dd15 100644
--- a/package.json
+++ b/package.json
@@ -5,23 +5,30 @@
"type": "module",
"scripts": {
"dev": "vite --host",
- "build": "tsc && vite build",
+ "build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@monaco-editor/react": "^4.7.0",
"@reduxjs/toolkit": "^2.9.2",
+ "@tailwindcss/typography": "^0.5.19",
"@types/react-redux": "^7.1.33",
"axios": "^1.12.2",
"clsx": "^2.1.1",
"framer-motion": "^11.9.0",
+ "highlight.js": "^11.11.1",
"monaco-editor": "^0.54.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",
+ "rehype-highlight": "^7.0.2",
+ "rehype-raw": "^7.0.0",
+ "rehype-sanitize": "^6.0.0",
+ "remark-gfm": "^4.0.1",
"tailwind-cn": "^1.0.2",
"tailwind-merge": "^1.14.0",
"tailwindcss": "^3.4.12"
diff --git a/src/App.tsx b/src/App.tsx
index eab18e1..6d0ec91 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,27 +1,31 @@
-import { Route, Routes } from "react-router-dom";
+import { Route, Routes } from 'react-router-dom';
// import { PrimaryButton } from "./components/button/PrimaryButton";
// import { SecondaryButton } from "./components/button/SecondaryButton";
// import { Checkbox } from "./components/checkbox/Checkbox";
// import { Input } from "./components/input/Input";
// import { Switch } from "./components/switch/Switch";
-import Home from "./pages/Home";
-import Mission from "./pages/Mission";
-import UploadMissionForm from "./views/mission/UploadMissionForm";
+import Home from './pages/Home';
+import Mission from './pages/Mission';
+import ArticleEditor from './pages/ArticleEditor';
+import Article from './pages/Article';
function App() {
- return (
-
-
-
- } />
- } />
- }/>
- } />
-
-
-
-
- );
+ return (
+
+
+
+ } />
+ } />
+ }
+ />
+ } />
+ } />
+
+
+
+ );
}
export default App;
diff --git a/src/assets/icons/account/clipboard.svg b/src/assets/icons/account/clipboard.svg
new file mode 100644
index 0000000..fb6e81b
--- /dev/null
+++ b/src/assets/icons/account/clipboard.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/icons/account/cup.svg b/src/assets/icons/account/cup.svg
new file mode 100644
index 0000000..330aeb0
--- /dev/null
+++ b/src/assets/icons/account/cup.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/icons/account/index.ts b/src/assets/icons/account/index.ts
new file mode 100644
index 0000000..5fa91fb
--- /dev/null
+++ b/src/assets/icons/account/index.ts
@@ -0,0 +1,5 @@
+import Clipboard from './clipboard.svg';
+import Cup from './cup.svg';
+import OpenBook from './openbook.svg';
+
+export { Clipboard, Cup, OpenBook };
diff --git a/src/assets/icons/account/openbook.svg b/src/assets/icons/account/openbook.svg
new file mode 100644
index 0000000..db632c4
--- /dev/null
+++ b/src/assets/icons/account/openbook.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/icons/auth/account.svg b/src/assets/icons/auth/account.svg
new file mode 100644
index 0000000..b4d5858
--- /dev/null
+++ b/src/assets/icons/auth/account.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/icons/auth/index.ts b/src/assets/icons/auth/index.ts
index e308253..0c604a5 100644
--- a/src/assets/icons/auth/index.ts
+++ b/src/assets/icons/auth/index.ts
@@ -1,3 +1,4 @@
-import Balloon from "./balloon.svg";
+import Balloon from './balloon.svg';
+import Account from './account.svg';
-export {Balloon};
\ No newline at end of file
+export { Balloon, Account };
diff --git a/src/assets/icons/groups/index.ts b/src/assets/icons/groups/index.ts
index 86bef2e..1913817 100644
--- a/src/assets/icons/groups/index.ts
+++ b/src/assets/icons/groups/index.ts
@@ -1,8 +1,8 @@
-import Book from "./book.png"
-import EyeClosed from "./eye-closed.svg";
-import EyeOpen from "./eye-open.png";
-import Edit from "./edit.svg";
-import UserAdd from "./user-profile-add.svg";
-import ChevroneDown from "./chevron-down.svg"
+import Book from './book.png';
+import EyeClosed from './eye-closed.svg';
+import EyeOpen from './eye-open.png';
+import Edit from './edit.svg';
+import UserAdd from './user-profile-add.svg';
+import ChevroneDown from './chevron-down.svg';
-export {Book, Edit, EyeClosed, EyeOpen, UserAdd, ChevroneDown}
\ No newline at end of file
+export { Book, Edit, EyeClosed, EyeOpen, UserAdd, ChevroneDown };
diff --git a/src/assets/icons/header/index.ts b/src/assets/icons/header/index.ts
index 5ea2a4b..333f6d9 100644
--- a/src/assets/icons/header/index.ts
+++ b/src/assets/icons/header/index.ts
@@ -1,5 +1,5 @@
-import arrowLeft from "./arrow-left-sm.svg";
-import chevroneLeft from "./chevron-left.svg"
-import chevroneRight from "./chevron-right.svg"
+import arrowLeft from './arrow-left-sm.svg';
+import chevroneLeft from './chevron-left.svg';
+import chevroneRight from './chevron-right.svg';
-export {arrowLeft, chevroneLeft, chevroneRight}
\ No newline at end of file
+export { arrowLeft, chevroneLeft, chevroneRight };
diff --git a/src/assets/icons/input/edit.svg b/src/assets/icons/input/edit.svg
new file mode 100644
index 0000000..27910a5
--- /dev/null
+++ b/src/assets/icons/input/edit.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/icons/input/index.ts b/src/assets/icons/input/index.ts
index 6409328..08cfb70 100644
--- a/src/assets/icons/input/index.ts
+++ b/src/assets/icons/input/index.ts
@@ -1,8 +1,17 @@
-import eyeClosed from "./eye-closed.svg"
-import eyeOpen from "./eye-open.png"
-import googleLogo from "./google-logo.svg"
-import upload from "./upload.svg"
-import chevroneDropDownList from "./chevron-drop-down.svg"
-import checkMark from "./check-mark.svg"
+import eyeClosed from './eye-closed.svg';
+import eyeOpen from './eye-open.png';
+import googleLogo from './google-logo.svg';
+import upload from './upload.svg';
+import chevroneDropDownList from './chevron-drop-down.svg';
+import checkMark from './check-mark.svg';
+import Edit from './edit.svg';
-export {eyeClosed, eyeOpen, googleLogo, upload, chevroneDropDownList, checkMark}
\ No newline at end of file
+export {
+ Edit,
+ eyeClosed,
+ eyeOpen,
+ googleLogo,
+ upload,
+ chevroneDropDownList,
+ checkMark,
+};
diff --git a/src/assets/icons/menu/index.ts b/src/assets/icons/menu/index.ts
index cbeba0a..2cbb7b1 100644
--- a/src/assets/icons/menu/index.ts
+++ b/src/assets/icons/menu/index.ts
@@ -1,8 +1,8 @@
-import Account from "./account.svg";
-import Clipboard from "./clipboard.svg";
-import Cup from "./cup.svg";
-import Home from "./home.svg";
-import Openbook from "./openbook.svg";
-import Users from "./users.svg";
+import Account from './account.svg';
+import Clipboard from './clipboard.svg';
+import Cup from './cup.svg';
+import Home from './home.svg';
+import Openbook from './openbook.svg';
+import Users from './users.svg';
-export {Account, Clipboard, Cup, Home, Openbook, Users};
\ No newline at end of file
+export { Account, Clipboard, Cup, Home, Openbook, Users };
diff --git a/src/assets/icons/missions/copy-icon.svg b/src/assets/icons/missions/copy-icon.svg
new file mode 100644
index 0000000..dd2147a
--- /dev/null
+++ b/src/assets/icons/missions/copy-icon.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/assets/icons/missions/index.ts b/src/assets/icons/missions/index.ts
index 30948a7..778fa2e 100644
--- a/src/assets/icons/missions/index.ts
+++ b/src/assets/icons/missions/index.ts
@@ -1,4 +1,5 @@
-import IconSuccess from "./icon-success.svg"
-import IconError from "./icon-error.svg"
+import IconSuccess from './icon-success.svg';
+import IconError from './icon-error.svg';
+import CopyIcon from './copy-icon.svg';
-export {IconError, IconSuccess}
\ No newline at end of file
+export { IconError, IconSuccess, CopyIcon };
diff --git a/src/assets/logos/index.ts b/src/assets/logos/index.ts
index 42f784e..5ea6c09 100644
--- a/src/assets/logos/index.ts
+++ b/src/assets/logos/index.ts
@@ -1,3 +1,3 @@
-import Logo from "./Logo.svg"
+import Logo from './Logo.svg';
-export {Logo}
\ No newline at end of file
+export { Logo };
diff --git a/src/axios.ts b/src/axios.ts
index 3da69b2..9a27882 100644
--- a/src/axios.ts
+++ b/src/axios.ts
@@ -1,24 +1,25 @@
-import axios from "axios";
+import axios from 'axios';
const instance = axios.create({
- baseURL: import.meta.env.VITE_API_URL,
- headers: {
- 'Content-Type': 'application/json'
- },
+ baseURL: import.meta.env.VITE_API_URL,
+ headers: {
+ 'Content-Type': 'application/json',
+ },
});
// Request interceptor: автоматически подставляет JWT, если есть
instance.interceptors.request.use(
- (config) => {
- const token = localStorage.getItem("jwt"); // или можно брать из Redux через store.getState()
- if (token) {
- config.headers.Authorization = `Bearer ${token}`;
- }
- return config;
- },
- (error) => {
- return Promise.reject(error);
- }
+ (config) => {
+ const token = localStorage.getItem('jwt'); // или можно брать из Redux через store.getState()
+
+ if (token) {
+ config.headers.Authorization = `Bearer ${token}`;
+ }
+ return config;
+ },
+ (error) => {
+ return Promise.reject(error);
+ },
);
export default instance;
diff --git a/src/components/button/PrimaryButton.tsx b/src/components/button/PrimaryButton.tsx
index 02e01c7..6a9423f 100644
--- a/src/components/button/PrimaryButton.tsx
+++ b/src/components/button/PrimaryButton.tsx
@@ -1,70 +1,92 @@
-import React from "react";
-import { cn } from "../../lib/cn";
+import React from 'react';
+import { cn } from '../../lib/cn';
interface ButtonProps {
- disabled?: boolean;
- text?: string;
- className?: string;
- onClick: () => void;
- children?: React.ReactNode;
+ disabled?: boolean;
+ text?: string;
+ className?: string;
+ onClick: (e: React.MouseEvent) => void;
+ children?: React.ReactNode;
+ color?: 'primary' | 'secondary' | 'error' | 'warning' | 'success';
}
-export const PrimaryButton: React.FC = ({
- disabled = false,
- text = "",
- className,
- onClick,
- children,
-}) => {
- return (
-
- );
+const ColorBgVariants = {
+ primary: 'bg-liquid-brightmain group-hover:ring-liquid-brightmain',
+ secondary: 'bg-liquid-darkmain group-hover:ring-liquid-darkmain',
+ error: 'bg-liquid-red group-hover:ring-liquid-red',
+ warning: 'bg-liquid-orange group-hover:ring-liquid-orange',
+ success: 'bg-liquid-green group-hover:ring-liquid-green',
+};
+
+const ColorTextVariants = {
+ primary: 'group-hover:text-liquid-brightmain ',
+ secondary: 'group-hover:text-liquid-brightmain ',
+ error: 'group-hover:text-liquid-red ',
+ warning: 'group-hover:text-liquid-orange ',
+ success: 'group-hover:text-liquid-green ',
+};
+
+export const PrimaryButton: React.FC = ({
+ disabled = false,
+ text = '',
+ className,
+ onClick,
+ children,
+ color = 'secondary',
+}) => {
+ return (
+
+ );
};
diff --git a/src/components/button/ReverseButton.tsx b/src/components/button/ReverseButton.tsx
new file mode 100644
index 0000000..67ddceb
--- /dev/null
+++ b/src/components/button/ReverseButton.tsx
@@ -0,0 +1,94 @@
+import React from 'react';
+import { cn } from '../../lib/cn';
+
+interface ButtonProps {
+ disabled?: boolean;
+ text?: string;
+ className?: string;
+ onClick: (e: React.MouseEvent) => void;
+ children?: React.ReactNode;
+ color?: 'primary' | 'secondary' | 'error' | 'warning' | 'success';
+}
+
+const ColorBgVariants = {
+ primary: 'group-hover:bg-liquid-brightmain ring-liquid-brightmain',
+ secondary: 'group-hover:bg-liquid-darkmain ring-liquid-darkmain',
+ error: 'group-hover:bg-liquid-red ring-liquid-red',
+ warning: 'group-hover:bg-liquid-orange ring-liquid-orange',
+ success: 'group-hover:bg-liquid-green ring-liquid-green',
+};
+
+const ColorTextVariants = {
+ primary: 'text-liquid-brightmain ',
+ secondary: 'text-liquid-brightmain ',
+ error: 'text-liquid-red ',
+ warning: 'text-liquid-orange ',
+ success: 'text-liquid-green ',
+};
+
+export const ReverseButton: React.FC = ({
+ disabled = false,
+ text = '',
+ className,
+ onClick,
+ children,
+ color = 'secondary',
+}) => {
+ return (
+
+ );
+};
diff --git a/src/components/button/SecondaryButton.tsx b/src/components/button/SecondaryButton.tsx
index fb92feb..e71ab94 100644
--- a/src/components/button/SecondaryButton.tsx
+++ b/src/components/button/SecondaryButton.tsx
@@ -1,69 +1,70 @@
-import React from "react";
-import { cn } from "../../lib/cn";
+import React from 'react';
+import { cn } from '../../lib/cn';
interface ButtonProps {
- disabled?: boolean;
- text?: string;
- className?: string;
- onClick: () => void;
- children?: React.ReactNode;
-
+ disabled?: boolean;
+ text?: string;
+ className?: string;
+ onClick: (e: React.MouseEvent) => void;
+ children?: React.ReactNode;
}
export const SecondaryButton: React.FC = ({
- disabled = false,
- text = "",
- className,
- onClick,
- children,
+ disabled = false,
+ text = '',
+ className,
+ onClick,
+ children,
}) => {
- return (
-
+ );
};
diff --git a/src/components/checkbox/Checkbox.tsx b/src/components/checkbox/Checkbox.tsx
index 0153384..180363e 100644
--- a/src/components/checkbox/Checkbox.tsx
+++ b/src/components/checkbox/Checkbox.tsx
@@ -1,168 +1,167 @@
-import React from "react";
-import { cn } from "../../lib/cn";
-import { motion } from "framer-motion";
+import React from 'react';
+import { cn } from '../../lib/cn';
+import { motion } from 'framer-motion';
const pathVariants = {
- hidden: {
- opacity: 0,
- pathLength: 0,
- },
- visible: {
- opacity: 1,
- pathLength: 1,
- transition: {
- delay: 0.15,
- duration: 0.4,
- ease: "easeInOut",
+ hidden: {
+ opacity: 0,
+ pathLength: 0,
+ },
+ visible: {
+ opacity: 1,
+ pathLength: 1,
+ transition: {
+ delay: 0.15,
+ duration: 0.4,
+ ease: 'easeInOut',
+ },
},
- },
};
const sizeVariants = {
- sm: "h-4 w-4",
- md: "h-5 w-5",
- lg: "h-6 w-6",
+ sm: 'h-4 w-4',
+ md: 'h-5 w-5',
+ lg: 'h-6 w-6',
};
const colorsVariants = {
- default: "bg-default",
- primary: "bg-liquid-brightmain",
- secondary: "bg-liquid-darkmain",
- success: "bg-liquid-green",
- warning: "bg-liquid-orange",
- danger: "bg-liquid-red",
+ default: 'bg-default',
+ primary: 'bg-liquid-brightmain',
+ secondary: 'bg-liquid-darkmain',
+ success: 'bg-liquid-green',
+ warning: 'bg-liquid-orange',
+ danger: 'bg-liquid-red',
};
-
const borderColorsVariants = {
- default: "border-default",
- primary: "border-liquid-brightmain",
- secondary: "border-liquid-darkmain",
- success: "border-liquid-green",
- warning: "border-liquid-orange",
- danger: "border-liquid-red",
+ default: 'border-default',
+ primary: 'border-liquid-brightmain',
+ secondary: 'border-liquid-darkmain',
+ success: 'border-liquid-green',
+ warning: 'border-liquid-orange',
+ danger: 'border-liquid-red',
};
const focuseOutlineVariants = {
- default: "[&:focus-visible+*]:outline-default",
- primary: "[&:focus-visible+*]:outline-liquid-brightmain",
- secondary: "[&:focus-visible+*]:outline-liquid-darkmain",
- success: "[&:focus-visible+*]:outline-liquid-green",
- warning: "[&:focus-visible+*]:outline-liquid-orange",
- danger: "[&:focus-visible+*]:outline-liquid-red",
+ default: '[&:focus-visible+*]:outline-default',
+ primary: '[&:focus-visible+*]:outline-liquid-brightmain',
+ secondary: '[&:focus-visible+*]:outline-liquid-darkmain',
+ success: '[&:focus-visible+*]:outline-liquid-green',
+ warning: '[&:focus-visible+*]:outline-liquid-orange',
+ danger: '[&:focus-visible+*]:outline-liquid-red',
};
const radiusVraiants = {
- none: "",
- sm: "rounded-[3.5px]",
- md: "rounded-[5px]",
- lg: "rounded-[7px]",
- full: "rounded-full",
+ none: '',
+ sm: 'rounded-[3.5px]',
+ md: 'rounded-[5px]',
+ lg: 'rounded-[7px]',
+ full: 'rounded-full',
};
interface CheckboxProps {
- size?: "sm" | "md" | "lg";
- radius?: "none" | "sm" | "md" | "lg" | "full";
- disabled?: boolean;
- color?:
- | "default"
- | "primary"
- | "secondary"
- | "success"
- | "warning"
- | "danger";
- label?: string;
- variant?: "default" | "label";
- className?: string;
- defaultState?: boolean;
- onChange: (state: boolean) => void;
+ size?: 'sm' | 'md' | 'lg';
+ radius?: 'none' | 'sm' | 'md' | 'lg' | 'full';
+ disabled?: boolean;
+ color?:
+ | 'default'
+ | 'primary'
+ | 'secondary'
+ | 'success'
+ | 'warning'
+ | 'danger';
+ label?: string;
+ variant?: 'default' | 'label';
+ className?: string;
+ defaultState?: boolean;
+ onChange: (state: boolean) => void;
}
export const Checkbox: React.FC = ({
- size = "md",
- radius = "md",
- disabled = false,
- color = "primary",
- label = "",
- variant = "label",
- className,
- onChange,
- defaultState = false,
+ size = 'md',
+ radius = 'md',
+ disabled = false,
+ color = 'primary',
+ label = '',
+ variant = 'label',
+ className,
+ onChange,
+ defaultState = false,
}) => {
- const [active, setActive] = React.useState(defaultState);
+ const [active, setActive] = React.useState(defaultState);
- React.useEffect(() => onChange(active), [active]);
+ React.useEffect(() => onChange(active), [active]);
- return (
-
-
-
{
- setActive(!active);
- }}
- />
-
-
-
-
- {variant == "label" && (
-
- {label}
-
- )}
-
- );
+ >
+
+
{
+ setActive(!active);
+ }}
+ />
+
+
+
+
+
+ {variant == 'label' && (
+
+ {label}
+
+ )}
+
+ );
};
diff --git a/src/components/drop-down-list/DropDownList.tsx b/src/components/drop-down-list/DropDownList.tsx
index 690dcfa..38f994e 100644
--- a/src/components/drop-down-list/DropDownList.tsx
+++ b/src/components/drop-down-list/DropDownList.tsx
@@ -1,7 +1,7 @@
-import React from "react";
-import { cn } from "../../lib/cn";
-import { checkMark, chevroneDropDownList } from "../../assets/icons/input";
-import { useClickOutside } from "../../hooks/useClickOutside";
+import React from 'react';
+import { cn } from '../../lib/cn';
+import { checkMark, chevroneDropDownList } from '../../assets/icons/input';
+import { useClickOutside } from '../../hooks/useClickOutside';
export interface DropDownListItem {
text: string;
@@ -18,15 +18,16 @@ interface DropDownListProps {
export const DropDownList: React.FC = ({
// disabled = false,
- className = "",
+ className = '',
onChange,
defaultState,
- items = [{ text: "", value: "" }],
+ items = [{ text: '', value: '' }],
}) => {
- if (items.length == 0)
- items.push({ text: "", value: "" });
+ if (items.length == 0) items.push({ text: '', value: '' });
- const [value, setValue] = React.useState(defaultState != undefined ? defaultState : items[0]);
+ const [value, setValue] = React.useState(
+ defaultState != undefined ? defaultState : items[0],
+ );
const [active, setActive] = React.useState(false);
React.useEffect(() => onChange(value.value), [value]);
@@ -37,67 +38,73 @@ export const DropDownList: React.FC = ({
setActive(false);
});
-
return (
-
-
+
{
setActive(!active);
- }
- }>
+ }}
+ >
{value.text}
-
-

-
+
+ className={cn(
+ ' absolute rounded-[10px] bg-liquid-lighter w-[180px] left-0 top-[48px] z-50 transition-all duration-300',
+ 'grid overflow-hidden',
+ active
+ ? 'grid-rows-[1fr] opacity-100'
+ : 'grid-rows-[0fr] opacity-0',
+ )}
+ >
-
-
- {items.map((v, i) =>
+
+ {items.map((v, i) => (
{
setValue(v);
setActive(false);
- }}>
+ }}
+ >
{v.text}
- {v.text == value.text &&
-

- }
+ {v.text == value.text && (
+

+ )}
- )}
+ ))}
);
-
};
diff --git a/src/components/input/DateRangeInput.tsx b/src/components/input/DateRangeInput.tsx
new file mode 100644
index 0000000..1a1c732
--- /dev/null
+++ b/src/components/input/DateRangeInput.tsx
@@ -0,0 +1,48 @@
+import React from 'react';
+
+interface DateRangeInputProps {
+ startLabel?: string;
+ endLabel?: string;
+ startValue?: string;
+ endValue?: string;
+ onChange: (field: 'startsAt' | 'endsAt', value: string) => void;
+ className?: string;
+}
+
+const DateRangeInput: React.FC
= ({
+ startLabel = 'Дата начала',
+ endLabel = 'Дата окончания',
+ startValue,
+ endValue,
+ onChange,
+ className = '',
+}) => {
+ return (
+
+ );
+};
+
+export default DateRangeInput;
diff --git a/src/components/input/Input.tsx b/src/components/input/Input.tsx
index 1b441f0..22588b6 100644
--- a/src/components/input/Input.tsx
+++ b/src/components/input/Input.tsx
@@ -1,78 +1,95 @@
-import React from "react";
-import { cn } from "../../lib/cn";
-import { eyeClosed, eyeOpen } from "../../assets/icons/input";
+import React from 'react';
+import { cn } from '../../lib/cn';
+import { eyeClosed, eyeOpen } from '../../assets/icons/input';
interface inputProps {
- name?: string;
- type: "text" | "email" | "password" | "first_name";
- error?: string;
- disabled?: boolean;
- required?: boolean;
- label?: string;
- placeholder?: string;
- className?: string;
- onChange: (state: string) => void;
- defaultState?: string;
- autocomplete?: string;
+ name?: string;
+ type: 'text' | 'email' | 'password' | 'first_name' | 'number';
+ 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 Input: React.FC = ({
- type = "text",
- error = "",
- // disabled = false,
- // required = false,
- label = "",
- placeholder = "",
- className = "",
- onChange,
- defaultState = "",
- name = "",
- autocomplete="",
+ type = 'text',
+ error = '',
+ // disabled = false,
+ // required = false,
+ label = '',
+ placeholder = '',
+ className = '',
+ onChange,
+ defaultState = '',
+ name = '',
+ autocomplete = '',
+ onKeyDown,
}) => {
- const [value, setValue] = React.useState(defaultState);
- const [visible, setVIsible] = React.useState(type != "password");
+ const [value, setValue] = React.useState(defaultState);
+ const [visible, setVIsible] = React.useState(type != 'password');
- React.useEffect(() => onChange(value), [value]);
+ React.useEffect(() => onChange(value), [value]);
+ React.useEffect(() => setValue(defaultState), [defaultState]);
+ return (
+
+
+ {label}
+
+
- return (
-
-
- {label}
-
-
-
{
- setValue(e.target.value);
- }} />
- {
- type == "password" &&
-

{
- setVIsible(!visible);
- }}/>
- }
-
-
-
- {error}
-
-
-
- );
-
+
+ {error}
+
+
+ );
};
diff --git a/src/components/modal/Modal.tsx b/src/components/modal/Modal.tsx
new file mode 100644
index 0000000..a70eaf9
--- /dev/null
+++ b/src/components/modal/Modal.tsx
@@ -0,0 +1,80 @@
+import React from 'react';
+import { motion, AnimatePresence } from 'framer-motion';
+import { cn } from '../../lib/cn';
+import { useClickOutside } from '../../hooks/useClickOutside';
+
+type ModalBackdrop = 'opaque' | 'blur';
+
+interface ModalProps {
+ className?: string;
+ children?: React.ReactNode;
+ backdrop?: ModalBackdrop;
+ open: boolean;
+ defaultOpen?: boolean;
+ onOpenChange: (state: boolean) => void;
+}
+
+const modalbgVariants = {
+ closed: { opacity: 0 },
+ open: { opacity: 1 },
+};
+
+const modalVariants = {
+ closed: { opacity: 0, scale: 0.9 },
+ open: { opacity: 1, scale: 1 },
+};
+
+export const Modal: React.FC = ({
+ children,
+ open,
+ backdrop,
+ className,
+ onOpenChange,
+}) => {
+ const ref = React.useRef(null);
+
+ useClickOutside(ref, () => {
+ onOpenChange(false);
+ });
+
+ return (
+
+
+ {open && (
+
+ )}
+
+
+
+ {open && (
+
+ {children}
+
+ )}
+
+
+
+ );
+};
diff --git a/src/components/router/ProtectedRoute.tsx b/src/components/router/ProtectedRoute.tsx
new file mode 100644
index 0000000..775a461
--- /dev/null
+++ b/src/components/router/ProtectedRoute.tsx
@@ -0,0 +1,12 @@
+// src/routes/ProtectedRoute.tsx
+import { Navigate, Outlet } from 'react-router-dom';
+import { useAppSelector } from '../../redux/hooks';
+
+export default function ProtectedRoute() {
+ const isAuthenticated = useAppSelector((state) => !!state.auth.jwt);
+ if (!isAuthenticated) {
+ return ;
+ }
+
+ return ;
+}
diff --git a/src/components/switch/Switch.tsx b/src/components/switch/Switch.tsx
index 8b812d8..f1bc0e7 100644
--- a/src/components/switch/Switch.tsx
+++ b/src/components/switch/Switch.tsx
@@ -1,187 +1,191 @@
-import React from "react";
-import { cn } from "../../lib/cn";
+import React from 'react';
+import { cn } from '../../lib/cn';
/* Варианты размера контейнера */
const sizeVariants = {
- sm: "h-6 w-10",
- md: "h-7 w-12",
- lg: "h-8 w-14",
+ sm: 'h-6 w-10',
+ md: 'h-7 w-12',
+ lg: 'h-8 w-14',
};
/* Варианты для скользящего шарика */
const switchVariants = {
- size: {
- sm: "h-4 w-4",
- md: "h-5 w-5",
- lg: "h-6 w-6",
- },
- activeSize: {
- sm: "group-active:w-5",
- md: "group-active:w-6",
- lg: "group-active:w-7",
- },
- iconSize: {
- sm: "h-3 w-3",
- md: "h-[0.875rem] w-[0.875rem]",
- lg: "h-4 w-4",
- },
+ size: {
+ sm: 'h-4 w-4',
+ md: 'h-5 w-5',
+ lg: 'h-6 w-6',
+ },
+ activeSize: {
+ sm: 'group-active:w-5',
+ md: 'group-active:w-6',
+ lg: 'group-active:w-7',
+ },
+ iconSize: {
+ sm: 'h-3 w-3',
+ md: 'h-[0.875rem] w-[0.875rem]',
+ lg: 'h-4 w-4',
+ },
};
const colorsVariants = {
- default: "bg-default",
- primary: "bg-liquid-brightmain",
- secondary: "bg-liquid-darkmain",
- success: "bg-liquid-green",
- warning: "bg-liquid-orange",
- danger: "bg-liquid-red",
+ default: 'bg-default',
+ primary: 'bg-liquid-brightmain',
+ secondary: 'bg-liquid-darkmain',
+ success: 'bg-liquid-green',
+ warning: 'bg-liquid-orange',
+ danger: 'bg-liquid-red',
};
const focuseOutlineVariants = {
- default: "[&:focus-visible+*]:outline-default",
- primary: "[&:focus-visible+*]:outline-liquid-brightmain",
- secondary: "[&:focus-visible+*]:outline-liquid-darkmain",
- success: "[&:focus-visible+*]:outline-liquid-green",
- warning: "[&:focus-visible+*]:outline-liquid-orange",
- danger: "[&:focus-visible+*]:outline-liquid-red",
+ default: '[&:focus-visible+*]:outline-default',
+ primary: '[&:focus-visible+*]:outline-liquid-brightmain',
+ secondary: '[&:focus-visible+*]:outline-liquid-darkmain',
+ success: '[&:focus-visible+*]:outline-liquid-green',
+ warning: '[&:focus-visible+*]:outline-liquid-orange',
+ danger: '[&:focus-visible+*]:outline-liquid-red',
};
/**
* Иконка солнца
*/
const sun = (
-
+
);
/**
* Иконка луны
*/
const moon = (
-
+
);
interface SwitchProps {
- size?: "sm" | "md" | "lg";
- disabled?: boolean;
- color?:
- | "default"
- | "primary"
- | "secondary"
- | "success"
- | "warning"
- | "danger";
- label?: string;
- variant?: "default" | "label" | "icon" | "theme";
- className?: string;
- defaultState?: boolean;
- onChange: (state: boolean) => void;
+ size?: 'sm' | 'md' | 'lg';
+ disabled?: boolean;
+ color?:
+ | 'default'
+ | 'primary'
+ | 'secondary'
+ | 'success'
+ | 'warning'
+ | 'danger';
+ label?: string;
+ variant?: 'default' | 'label' | 'icon' | 'theme';
+ className?: string;
+ defaultState?: boolean;
+ onChange: (state: boolean) => void;
}
export const Switch: React.FC = ({
- size = "sm",
- disabled = false,
- color = "primary",
- label = "",
- variant = "default",
- className,
- onChange,
- defaultState = false,
+ size = 'sm',
+ disabled = false,
+ color = 'primary',
+ label = '',
+ variant = 'default',
+ className,
+ onChange,
+ defaultState = false,
}) => {
- const [active, setActive] = React.useState(defaultState);
+ const [active, setActive] = React.useState(defaultState);
- React.useEffect(() => onChange(active), [active]);
+ React.useEffect(() => onChange(active), [active]);
- return (
-
- );
+
+
+ {/* Шарик */}
+
+ {variant == 'theme' && (
+ <>
+
+ {moon}
+
+
+ {sun}
+
+ >
+ )}
+
+
+
+ {variant == 'label' && (
+
+ {label}
+
+ )}
+
+ );
};
diff --git a/src/config/colors.ts b/src/config/colors.ts
index 8d3f780..86352cf 100644
--- a/src/config/colors.ts
+++ b/src/config/colors.ts
@@ -1,14 +1,14 @@
export default {
- liquid: {
- brightmain: "var(--color-liquid-brightmain)",
- darkmain: "var(--color-liquid-darkmain)",
- darker: "var(--color-liquid-darker)",
- background: "var(--color-liquid-background)",
- lighter: "var(--color-liquid-lighter)",
- white: "var(--color-liquid-white)",
- red: "var(--color-liquid-red)",
- green: "var(--color-liquid-green)",
- light: "var(--color-liquid-light)",
- orange: "var(--color-liquid-orange)",
- }
+ liquid: {
+ brightmain: 'var(--color-liquid-brightmain)',
+ darkmain: 'var(--color-liquid-darkmain)',
+ darker: 'var(--color-liquid-darker)',
+ background: 'var(--color-liquid-background)',
+ lighter: 'var(--color-liquid-lighter)',
+ white: 'var(--color-liquid-white)',
+ red: 'var(--color-liquid-red)',
+ green: 'var(--color-liquid-green)',
+ light: 'var(--color-liquid-light)',
+ orange: 'var(--color-liquid-orange)',
+ },
};
diff --git a/src/hooks/useClickOutside.ts b/src/hooks/useClickOutside.ts
index fb27289..e56bf50 100644
--- a/src/hooks/useClickOutside.ts
+++ b/src/hooks/useClickOutside.ts
@@ -1,18 +1,21 @@
-import React from "react";
+import React from 'react';
-export const useClickOutside = (ref: React.RefObject
, onClickOutside: () => void) => {
- React.useEffect(() => {
- const handleClickOutside = (event: MouseEvent | TouchEvent) => {
- if (ref.current && !ref.current.contains(event.target)) {
- onClickOutside();
- }
- }
+export const useClickOutside = (
+ ref: React.RefObject,
+ onClickOutside: () => void,
+) => {
+ React.useEffect(() => {
+ const handleClickOutside = (event: MouseEvent | TouchEvent) => {
+ if (ref.current && !ref.current.contains(event.target)) {
+ onClickOutside();
+ }
+ };
- document.addEventListener("mousedown", handleClickOutside);
- document.addEventListener("touchstart", handleClickOutside);
- return () => {
- document.removeEventListener("mousedown", handleClickOutside);
- document.removeEventListener("touchstart", handleClickOutside);
- }
- }, [ref, onClickOutside]);
-}
\ No newline at end of file
+ document.addEventListener('mousedown', handleClickOutside);
+ document.addEventListener('touchstart', handleClickOutside);
+ return () => {
+ document.removeEventListener('mousedown', handleClickOutside);
+ document.removeEventListener('touchstart', handleClickOutside);
+ };
+ }, [ref, onClickOutside]);
+};
diff --git a/src/hooks/useQuery.ts b/src/hooks/useQuery.ts
new file mode 100644
index 0000000..63ccae5
--- /dev/null
+++ b/src/hooks/useQuery.ts
@@ -0,0 +1,7 @@
+import { useMemo } from 'react';
+import { useLocation } from 'react-router-dom';
+
+export function useQuery() {
+ const { search } = useLocation();
+ return useMemo(() => new URLSearchParams(search), [search]);
+}
diff --git a/src/lib/cn.ts b/src/lib/cn.ts
index 95b19a8..883213c 100644
--- a/src/lib/cn.ts
+++ b/src/lib/cn.ts
@@ -1,6 +1,6 @@
-import { ClassValue, clsx } from "clsx";
-import { twMerge } from "tailwind-merge";
-
+import { ClassValue, clsx } from 'clsx';
+import { twMerge } from 'tailwind-merge';
+
export function cn(...inputs: ClassValue[]) {
- return twMerge(clsx(inputs));
-}
\ No newline at end of file
+ return twMerge(clsx(inputs));
+}
diff --git a/src/main.tsx b/src/main.tsx
index abace85..5e19935 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -1,16 +1,16 @@
-import { createRoot } from "react-dom/client";
-import App from "./App.tsx";
-import "./styles/index.css";
-import "./styles/palette/theme-dark.css";
-import "./styles/palette/theme-light.css";
-import { BrowserRouter } from "react-router-dom";
-import { Provider } from "react-redux";
-import { store } from "./redux/store";
+import { createRoot } from 'react-dom/client';
+import App from './App.tsx';
+import './styles/index.css';
+import './styles/palette/theme-dark.css';
+import './styles/palette/theme-light.css';
+import { BrowserRouter } from 'react-router-dom';
+import { Provider } from 'react-redux';
+import { store } from './redux/store';
-createRoot(document.getElementById("root")!).render(
-
-
-
-
-
+createRoot(document.getElementById('root')!).render(
+
+
+
+
+ ,
);
diff --git a/src/pages/Article.tsx b/src/pages/Article.tsx
new file mode 100644
index 0000000..44b910a
--- /dev/null
+++ b/src/pages/Article.tsx
@@ -0,0 +1,73 @@
+import { useParams, Navigate } from 'react-router-dom';
+import { useAppDispatch, useAppSelector } from '../redux/hooks';
+import Header from '../views/article/Header';
+import { useEffect } from 'react';
+import { fetchArticleById } from '../redux/slices/articles';
+import MarkdownPreview from '../views/articleeditor/MarckDownPreview';
+import { useQuery } from '../hooks/useQuery';
+
+const Article = () => {
+ // Получаем параметры из URL
+ const { articleId } = useParams<{ articleId: string }>();
+ const articleIdNumber = Number(articleId);
+
+ const query = useQuery();
+ const back = query.get('back') ?? undefined;
+
+ if (!articleId || isNaN(articleIdNumber)) {
+ if (back) return ;
+ return ;
+ }
+ const dispatch = useAppDispatch();
+ const article = useAppSelector((state) => state.articles.currentArticle);
+ const status = useAppSelector((state) => state.articles.statuses.fetchById);
+
+ useEffect(() => {
+ dispatch(fetchArticleById(articleIdNumber));
+ }, [articleIdNumber]);
+
+ return (
+
+
+
+
+
+
+ {status == 'loading' || !article ? (
+
Загрузка...
+ ) : (
+
+
+
+ {article.name}
+
+
+ #{article.id}
+
+
+ {article.tags.length && (
+
+ {article.tags.map((v, i) => (
+
+ {v}
+
+ ))}
+
+ )}
+
+
+ )}
+
+
+
+
+ );
+};
+
+export default Article;
diff --git a/src/pages/ArticleEditor.tsx b/src/pages/ArticleEditor.tsx
new file mode 100644
index 0000000..79002b8
--- /dev/null
+++ b/src/pages/ArticleEditor.tsx
@@ -0,0 +1,233 @@
+import { useNavigate } from 'react-router-dom';
+import Header from '../views/articleeditor/Header';
+import MarkdownEditor from '../views/articleeditor/Editor';
+import { useEffect, useState } from 'react';
+import { PrimaryButton } from '../components/button/PrimaryButton';
+import MarkdownPreview from '../views/articleeditor/MarckDownPreview';
+import { Input } from '../components/input/Input';
+import { useAppDispatch, useAppSelector } from '../redux/hooks';
+import {
+ createArticle,
+ deleteArticle,
+ fetchArticleById,
+ setArticlesStatus,
+ updateArticle,
+} from '../redux/slices/articles';
+import { useQuery } from '../hooks/useQuery';
+import { ReverseButton } from '../components/button/ReverseButton';
+
+const ArticleEditor = () => {
+ const navigate = useNavigate();
+ const dispatch = useAppDispatch();
+
+ 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 [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)) {
+ setTags([...tags, newTag]);
+ setTagInput('');
+ }
+ };
+
+ const removeTag = (tagToRemove: string) => {
+ setTags(tags.filter((tag) => tag !== tagToRemove));
+ };
+
+ useEffect(() => {
+ if (statusCreate == 'successful') {
+ dispatch(setArticlesStatus({ key: 'create', status: 'idle' }));
+ navigate(back ? 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');
+ }
+ }, [statusUpdate]);
+
+ useEffect(() => {
+ if (articleId) {
+ dispatch(fetchArticleById(articleId));
+ }
+ }, [articleId]);
+
+ useEffect(() => {
+ if (article && refactor) {
+ setCode(article?.content || '');
+ setName(article?.name || '');
+ setTags(article?.tags || []);
+ }
+ }, [article]);
+
+ return (
+
+ {activeEditor ? (
+
{
+ setActiveEditor(false);
+ }}
+ />
+ ) : (
+
+ );
+};
+
+export default ArticleEditor;
diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx
index 4058f4a..9cebabb 100644
--- a/src/pages/Home.tsx
+++ b/src/pages/Home.tsx
@@ -1,51 +1,82 @@
// import React from "react";
-import { Route, Routes } from "react-router-dom";
-import Login from "../views/home/auth/Login";
-import Register from "../views/home/auth/Register";
-import Menu from "../views/home/menu/Menu";
-import { useAppDispatch, useAppSelector } from "../redux/hooks";
-import { useEffect } from "react";
-import { fetchWhoAmI, logout } from "../redux/slices/auth";
-import Missions from "../views/home/missions/Missions";
-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 { Route, Routes } from 'react-router-dom';
+import Login from '../views/home/auth/Login';
+import Register from '../views/home/auth/Register';
+import Menu from '../views/home/menu/Menu';
+import { useAppDispatch, useAppSelector } from '../redux/hooks';
+import { useEffect } from 'react';
+import { fetchWhoAmI, logout } from '../redux/slices/auth';
+import Missions from '../views/home/missions/Missions';
+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 Contest from '../views/home/contest/Contest';
+import Account from '../views/home/account/Account';
+import ProtectedRoute from '../components/router/ProtectedRoute';
const Home = () => {
- const name = useAppSelector((state) => state.auth.username);
- const jwt = useAppSelector((state) => state.auth.jwt);
- const dispatch = useAppDispatch();
+ const name = useAppSelector((state) => state.auth.username);
+ const jwt = useAppSelector((state) => state.auth.jwt);
+ const dispatch = useAppDispatch();
- useEffect(() => {
- dispatch(fetchWhoAmI());
- }, [jwt])
+ useEffect(() => {
+ dispatch(fetchWhoAmI());
+ }, [jwt]);
+ return (
+
+
+
+
+
+
+ }>
+ } />
+
- return (
-
-
-
-
-
-
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- {name} {dispatch(logout())}}>выйти>} />
-
-
- {
-
- } />
-
- }
-
- );
+
} />
+
} />
+
} />
+
} />
+
} />
+
} />
+
} />
+
} />
+
+ {jwt}
+ {
+ if (jwt)
+ navigator.clipboard.writeText(jwt);
+ }}
+ text="скопировать токен"
+ className="pt-[20px]"
+ />
+ {name}
+ {
+ dispatch(logout());
+ }}
+ >
+ выйти
+
+ >
+ }
+ />
+
+
+ {
+
+ } />
+
+ }
+
+ );
};
export default Home;
diff --git a/src/pages/Mission.tsx b/src/pages/Mission.tsx
index 01814da..b09c25a 100644
--- a/src/pages/Mission.tsx
+++ b/src/pages/Mission.tsx
@@ -1,6 +1,6 @@
import { useParams, Navigate } from 'react-router-dom';
import CodeEditor from '../views/mission/codeeditor/CodeEditor';
-import Statement, { StatementData } from '../views/mission/statement/Statement';
+import Statement from '../views/mission/statement/Statement';
import { PrimaryButton } from '../components/button/PrimaryButton';
import { useEffect, useRef, useState } from 'react';
import { useAppDispatch, useAppSelector } from '../redux/hooks';
@@ -8,181 +8,203 @@ import { fetchMySubmitsByMission, submitMission } from '../redux/slices/submit';
import { fetchMissionById } from '../redux/slices/missions';
import Header from '../views/mission/statement/Header';
import MissionSubmissions from '../views/mission/statement/MissionSubmissions';
+import { useQuery } from '../hooks/useQuery';
const Mission = () => {
+ const dispatch = useAppDispatch();
- const dispatch = useAppDispatch();
+ // Получаем параметры из URL
+ const { missionId } = useParams<{ missionId: string }>();
+ const mission = useAppSelector((state) => state.missions.currentMission);
+ const missionIdNumber = Number(missionId);
- // Получаем параметры из URL
- const { missionId } = useParams<{ missionId: string }>();
- const mission = useAppSelector((state) => state.missions.currentMission);
- const missionIdNumber = Number(missionId);
- if (!missionId || isNaN(missionIdNumber)) {
- return ;
- }
+ const query = useQuery();
+ const back = query.get('back') ?? undefined;
- const [code, setCode] = useState("");
- const [language, setLanguage] = useState("");
-
- const pollingRef = useRef(null);
- const submissions = useAppSelector((state) => state.submin.submitsById[missionIdNumber] || []);
-
- useEffect(() => {
- dispatch(fetchMissionById(missionIdNumber));
- dispatch(fetchMySubmitsByMission(missionIdNumber));
- }, [missionIdNumber]);
-
- useEffect(() => {
- return () => {
- if (pollingRef.current) {
- clearInterval(pollingRef.current);
- pollingRef.current = null;
- }
- };
- }, []);
-
-
- useEffect(() => {
- if (submissions.length === 0) return;
-
- const hasWaiting = submissions.some(
- s => s.solution.status === "Waiting" || s.solution.testerState === "Waiting"
- );
-
- if (hasWaiting) {
- startPolling();
+ if (!missionId || isNaN(missionIdNumber)) {
+ if (back) return ;
+ return ;
}
- }, [submissions]);
+ const [code, setCode] = useState('');
+ const [language, setLanguage] = useState('');
- if (!mission || !mission.statements || mission.statements.length === 0) {
- return Загрузка...
;
- }
-
-
-
- interface StatementData {
- id: number;
- legend?: string;
- timeLimit?: number;
- output?: string;
- input?: string;
- sampleTests?: any[];
- name?: string;
- memoryLimit?: number;
- tags?: string[];
- notes?: string;
- html?: string;
- mediaFiles?: any[];
- }
-
- let statementData: StatementData = { id: mission.id };
-
- try {
- // 1. Берём первый statement с форматом Latex и языком russian
- const latexStatement = mission.statements.find(
- (stmt: any) => stmt && stmt.language === "russian" && stmt.format === "Latex"
+ const pollingRef = useRef(null);
+ const submissions = useAppSelector(
+ (state) => state.submin.submitsById[missionIdNumber] || [],
);
+ const submissionsRef = useRef(submissions);
- // 2. Берём первый statement с форматом Html и языком russian
- const htmlStatement = mission.statements.find(
- (stmt: any) => stmt && stmt.language === "russian" && stmt.format === "Html"
- );
+ const startPolling = () => {
+ if (pollingRef.current) return;
- if (!latexStatement) throw new Error("Не найден блок Latex на русском");
- if (!htmlStatement) throw new Error("Не найден блок Html на русском");
+ pollingRef.current = setInterval(async () => {
+ dispatch(fetchMySubmitsByMission(missionIdNumber));
- // 3. Парсим данные из problem-properties.json
- const statementTexts = JSON.parse(latexStatement.statementTexts["problem-properties.json"]);
-
- statementData = {
- id: missionIdNumber,
- legend: statementTexts.legend,
- timeLimit: statementTexts.timeLimit,
- output: statementTexts.output,
- input: statementTexts.input,
- sampleTests: statementTexts.sampleTests,
- name: statementTexts.name,
- memoryLimit: statementTexts.memoryLimit,
- tags: mission.tags,
- notes: statementTexts.notes,
- html: htmlStatement.statementTexts["problem.html"],
- mediaFiles: latexStatement.mediaFiles
+ const hasWaiting = submissionsRef.current.some(
+ (s: any) =>
+ s.solution.status == 'Waiting' ||
+ s.solution.testerState === 'Waiting' ||
+ s.solution.status === 'Compiling' ||
+ s.solution.testerState === 'Compiling',
+ );
+ if (!hasWaiting) {
+ // Всё проверено — стоп
+ if (pollingRef.current) {
+ clearInterval(pollingRef.current);
+ pollingRef.current = null;
+ }
+ }
+ }, 5000); // 10 секунд
};
- } catch (err) {
- console.error("Ошибка парсинга statementTexts:", err);
- }
+ useEffect(() => {
+ dispatch(fetchMissionById(missionIdNumber));
+ dispatch(fetchMySubmitsByMission(missionIdNumber));
+ }, [missionIdNumber]);
+ useEffect(() => {}, [submissions]);
+ useEffect(() => {
+ return () => {
+ if (pollingRef.current) {
+ clearInterval(pollingRef.current);
+ pollingRef.current = null;
+ }
+ };
+ }, []);
- const startPolling = () => {
- if (pollingRef.current)
- return;
+ useEffect(() => {
+ submissionsRef.current = submissions;
- pollingRef.current = setInterval(async () => {
- dispatch(fetchMySubmitsByMission(missionIdNumber));
+ if (submissions.length) {
+ const hasWaiting = submissions.some(
+ (s) =>
+ s.solution.status === 'Waiting' ||
+ s.solution.testerState === 'Waiting' ||
+ s.solution.status === 'Compiling' ||
+ s.solution.testerState === 'Compiling',
+ );
- const hasWaiting = submissions.some(
- (s: any) => s.solution.status == "Waiting" || s.solution.testerState === "Waiting"
- );
- if (!hasWaiting) {
- // Всё проверено — стоп
- if (pollingRef.current) {
- clearInterval(pollingRef.current);
- pollingRef.current = null;
+ if (hasWaiting) {
+ startPolling();
+ }
}
- }
- }, 5000); // 10 секунд
- };
+ }, [submissions]);
+ if (!mission || !mission.statements || mission.statements.length === 0) {
+ return Загрузка...
;
+ }
+ interface StatementData {
+ id: number;
+ legend?: string;
+ timeLimit?: number;
+ output?: string;
+ input?: string;
+ sampleTests?: any[];
+ name?: string;
+ memoryLimit?: number;
+ tags?: string[];
+ notes?: string;
+ html?: string;
+ mediaFiles?: any[];
+ }
- return (
+ let statementData: StatementData = { id: mission.id };
-
-
-
-
+ try {
+ // 1. Берём первый statement с форматом Latex и языком russian
+ const latexStatement = mission.statements.find(
+ (stmt: any) =>
+ stmt && stmt.language === 'russian' && stmt.format === 'Latex',
+ );
-
-
-
+ stmt && stmt.language === 'russian' && stmt.format === 'Html',
+ );
- />
+ if (!latexStatement) throw new Error('Не найден блок Latex на русском');
+ if (!htmlStatement) throw new Error('Не найден блок Html на русском');
+
+ // 3. Парсим данные из problem-properties.json
+ const statementTexts = JSON.parse(
+ latexStatement.statementTexts['problem-properties.json'],
+ );
+
+ statementData = {
+ id: missionIdNumber,
+ legend: statementTexts.legend,
+ timeLimit: statementTexts.timeLimit,
+ output: statementTexts.output,
+ input: statementTexts.input,
+ sampleTests: statementTexts.sampleTests,
+ name: statementTexts.name,
+ memoryLimit: statementTexts.memoryLimit,
+ tags: mission.tags,
+ notes: statementTexts.notes,
+ html: htmlStatement.statementTexts['problem.html'],
+ mediaFiles: latexStatement.mediaFiles,
+ };
+ } catch (err) {
+ console.error('Ошибка парсинга statementTexts:', err);
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {
+ setCode(value);
+ }}
+ onChangeLanguage={(value: string) => {
+ setLanguage(value);
+ }}
+ />
+
+
+
{
+ await dispatch(
+ submitMission({
+ missionId: missionIdNumber,
+ language: language,
+ languageVersion: 'latest',
+ sourceCode: code,
+ contestId: null,
+ }),
+ ).unwrap();
+ dispatch(
+ fetchMySubmitsByMission(
+ missionIdNumber,
+ ),
+ );
+ }}
+ />
+
+
+
+
+
+
+
+
-
-
-
-
- { setCode(value); }}
- onChangeLanguage={((value: string) => { setLanguage(value); })}
- />
-
-
-
{
- await dispatch(submitMission({
- missionId: missionIdNumber,
- language: language,
- languageVersion: "latest",
- sourceCode: code,
- contestId: null,
-
- })).unwrap();
- dispatch(fetchMySubmitsByMission(missionIdNumber));
- }} />
-
-
-
-
-
-
-
-
-
- );
+ );
};
export default Mission;
diff --git a/src/redux/slices/account.ts b/src/redux/slices/account.ts
new file mode 100644
index 0000000..e69de29
diff --git a/src/redux/slices/articles.ts b/src/redux/slices/articles.ts
new file mode 100644
index 0000000..73c59df
--- /dev/null
+++ b/src/redux/slices/articles.ts
@@ -0,0 +1,298 @@
+import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
+import axios from '../../axios';
+
+// ─── Типы ────────────────────────────────────────────
+
+type Status = 'idle' | 'loading' | 'successful' | 'failed';
+
+export interface Article {
+ id: number;
+ authorId: number;
+ name: string;
+ content: string;
+ tags: string[];
+ createdAt: string;
+ updatedAt: string;
+}
+
+interface ArticlesState {
+ articles: Article[];
+ currentArticle?: Article;
+ hasNextPage: boolean;
+ statuses: {
+ create: Status;
+ update: Status;
+ delete: Status;
+ fetchAll: Status;
+ fetchById: Status;
+ };
+ error: string | null;
+}
+
+const initialState: ArticlesState = {
+ articles: [],
+ currentArticle: undefined,
+ hasNextPage: false,
+ statuses: {
+ create: 'idle',
+ update: 'idle',
+ delete: 'idle',
+ fetchAll: 'idle',
+ fetchById: 'idle',
+ },
+ error: null,
+};
+
+// ─── Async Thunks ─────────────────────────────────────
+
+// POST /articles
+export const createArticle = createAsyncThunk(
+ 'articles/createArticle',
+ async (
+ {
+ name,
+ content,
+ tags,
+ }: { name: string; content: string; tags: string[] },
+ { rejectWithValue },
+ ) => {
+ try {
+ const response = await axios.post('/articles', {
+ name,
+ content,
+ tags,
+ });
+ return response.data as Article;
+ } catch (err: any) {
+ return rejectWithValue(
+ err.response?.data?.message || 'Ошибка при создании статьи',
+ );
+ }
+ },
+);
+
+// PUT /articles/{articleId}
+export const updateArticle = createAsyncThunk(
+ 'articles/updateArticle',
+ async (
+ {
+ articleId,
+ name,
+ content,
+ tags,
+ }: { articleId: number; name: string; content: string; tags: string[] },
+ { rejectWithValue },
+ ) => {
+ try {
+ const response = await axios.put(`/articles/${articleId}`, {
+ name,
+ content,
+ tags,
+ });
+ return response.data as Article;
+ } catch (err: any) {
+ return rejectWithValue(
+ err.response?.data?.message || 'Ошибка при обновлении статьи',
+ );
+ }
+ },
+);
+
+// DELETE /articles/{articleId}
+export const deleteArticle = createAsyncThunk(
+ 'articles/deleteArticle',
+ async (articleId: number, { rejectWithValue }) => {
+ try {
+ await axios.delete(`/articles/${articleId}`);
+ return articleId;
+ } catch (err: any) {
+ return rejectWithValue(
+ err.response?.data?.message || 'Ошибка при удалении статьи',
+ );
+ }
+ },
+);
+
+// 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 ────────────────────────────────────────────
+
+const articlesSlice = createSlice({
+ name: 'articles',
+ initialState,
+ reducers: {
+ clearCurrentArticle: (state) => {
+ state.currentArticle = undefined;
+ },
+ setArticlesStatus: (
+ state,
+ action: PayloadAction<{
+ key: keyof ArticlesState['statuses'];
+ status: Status;
+ }>,
+ ) => {
+ const { key, status } = action.payload;
+ state.statuses[key] = 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 ───
+ builder.addCase(fetchArticles.pending, (state) => {
+ state.statuses.fetchAll = 'loading';
+ state.error = null;
+ });
+ 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;
+ },
+ );
+
+ // ─── FETCH ARTICLE BY ID ───
+ builder.addCase(fetchArticleById.pending, (state) => {
+ state.statuses.fetchById = 'loading';
+ state.error = null;
+ });
+ builder.addCase(
+ fetchArticleById.fulfilled,
+ (state, action: PayloadAction) => {
+ state.statuses.fetchById = 'successful';
+ state.currentArticle = action.payload;
+ },
+ );
+ builder.addCase(
+ fetchArticleById.rejected,
+ (state, action: PayloadAction) => {
+ state.statuses.fetchById = 'failed';
+ state.error = action.payload;
+ },
+ );
+ },
+});
+
+export const { clearCurrentArticle, setArticlesStatus } = articlesSlice.actions;
+export const articlesReducer = articlesSlice.reducer;
diff --git a/src/redux/slices/auth.ts b/src/redux/slices/auth.ts
index 41a31ce..9b04ff6 100644
--- a/src/redux/slices/auth.ts
+++ b/src/redux/slices/auth.ts
@@ -1,188 +1,276 @@
-import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit";
-import axios from "../../axios";
+import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
+import axios from '../../axios';
-// Типы данных
-interface AuthState {
- jwt: string | null;
- refreshToken: string | null;
- username: string | null;
- status: "idle" | "loading" | "successful" | "failed";
- error: string | null;
+// 🔹 Декодирование JWT
+function decodeJwt(token: string) {
+ const [, payload] = token.split('.');
+ const json = atob(payload.replace(/-/g, '+').replace(/_/g, '/'));
+ return JSON.parse(decodeURIComponent(escape(json)));
}
-// Инициализация состояния
+// 🔹 Типы
+interface AuthState {
+ jwt: string | null;
+ refreshToken: string | null;
+ username: string | null;
+ email: string | null;
+ id: string | null;
+ status: 'idle' | 'loading' | 'successful' | 'failed';
+ error: string | null;
+}
+
+// 🔹 Инициализация состояния с синхронной загрузкой из localStorage
+const jwtFromStorage = localStorage.getItem('jwt');
+const refreshTokenFromStorage = localStorage.getItem('refreshToken');
+
const initialState: AuthState = {
- jwt: null,
- refreshToken: null,
- username: null,
- status: "idle",
- error: null,
+ jwt: jwtFromStorage || null,
+ refreshToken: refreshTokenFromStorage || null,
+ username: null,
+ email: null,
+ id: null,
+ status: 'idle',
+ error: null,
};
-// AsyncThunk: Регистрация
+// Если токен есть, подставляем в axios и декодируем
+if (jwtFromStorage) {
+ axios.defaults.headers.common['Authorization'] = `Bearer ${jwtFromStorage}`;
+ try {
+ const decoded = decodeJwt(jwtFromStorage);
+ initialState.username =
+ decoded[
+ 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name'
+ ] || null;
+ initialState.email =
+ decoded[
+ 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress'
+ ] || null;
+ initialState.id =
+ decoded[
+ 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier'
+ ] || null;
+ } catch {
+ localStorage.removeItem('jwt');
+ localStorage.removeItem('refreshToken');
+ delete axios.defaults.headers.common['Authorization'];
+ }
+}
+
+// 🔹 AsyncThunk-ы (login/register/refresh/whoami) остаются как были
export const registerUser = createAsyncThunk(
- "auth/register",
- async (
- { username, email, password }: { username: string; email: string; password: string },
- { rejectWithValue }
- ) => {
- try {
- const response = await axios.post("/authentication/register", { username, email, password });
- return response.data; // { jwt, refreshToken }
- } catch (err: any) {
- return rejectWithValue(err.response?.data?.message || "Registration failed");
- }
- }
-);
-
-// AsyncThunk: Логин
-export const loginUser = createAsyncThunk(
- "auth/login",
- async (
- { username, password }: { username: string; password: string },
- { rejectWithValue }
- ) => {
- try {
- const response = await axios.post("/authentication/login", { username, password });
- return response.data; // { jwt, refreshToken }
- } catch (err: any) {
- return rejectWithValue(err.response?.data?.message || "Login failed");
- }
- }
-);
-
-// AsyncThunk: Обновление токена
-export const refreshToken = createAsyncThunk(
- "auth/refresh",
- async ({ refreshToken }: { refreshToken: string }, { rejectWithValue }) => {
- try {
- const response = await axios.post("/authentication/refresh", { refreshToken });
- return response.data; // { username }
- } catch (err: any) {
- return rejectWithValue(err.response?.data?.message || "Refresh token failed");
- }
- }
-);
-
-// AsyncThunk: Получение информации о пользователе
-export const fetchWhoAmI = createAsyncThunk(
- "auth/whoami",
- async (_, { rejectWithValue }) => {
- try {
- const response = await axios.get("/authentication/whoami");
- return response.data; // { username }
- } catch (err: any) {
- return rejectWithValue(err.response?.data?.message || "Failed to fetch user info");
- }
- }
-);
-
-// AsyncThunk: Загрузка токенов из localStorage
-export const loadTokensFromLocalStorage = createAsyncThunk(
- "auth/loadTokens",
- async (_, { dispatch }) => {
- const jwt = localStorage.getItem("jwt");
- const refreshToken = localStorage.getItem("refreshToken");
-
- if (jwt && refreshToken) {
- axios.defaults.headers.common['Authorization'] = `Bearer ${jwt}`;
- return { jwt, refreshToken };
- } else {
- return { jwt: null, refreshToken: null };
- }
- }
-);
-
-// Slice
-const authSlice = createSlice({
- name: "auth",
- initialState,
- reducers: {
- logout: (state) => {
- state.jwt = null;
- state.refreshToken = null;
- state.username = null;
- state.status = "idle";
- state.error = null;
- localStorage.removeItem("jwt");
- localStorage.removeItem("refreshToken");
- delete axios.defaults.headers.common['Authorization'];
+ 'auth/register',
+ async (
+ {
+ username,
+ email,
+ password,
+ }: { username: string; email: string; password: string },
+ { rejectWithValue },
+ ) => {
+ try {
+ const response = await axios.post('/authentication/register', {
+ username,
+ email,
+ password,
+ });
+ return response.data;
+ } catch (err: any) {
+ return rejectWithValue(
+ err.response?.data?.message || 'Registration failed',
+ );
+ }
},
- },
- extraReducers: (builder) => {
- // Регистрация
- builder.addCase(registerUser.pending, (state) => {
- state.status = "loading";
- state.error = null;
- });
- builder.addCase(registerUser.fulfilled, (state, action: PayloadAction<{ jwt: string; refreshToken: string }>) => {
- state.status = "successful";
- state.jwt = action.payload.jwt;
- state.refreshToken = action.payload.refreshToken;
- axios.defaults.headers.common['Authorization'] = `Bearer ${action.payload.jwt}`;
- localStorage.setItem("jwt", action.payload.jwt);
- localStorage.setItem("refreshToken", action.payload.refreshToken);
- });
- builder.addCase(registerUser.rejected, (state, action: PayloadAction) => {
- state.status = "failed";
- state.error = action.payload;
- });
+);
- // Логин
- builder.addCase(loginUser.pending, (state) => {
- state.status = "loading";
- state.error = null;
- });
- builder.addCase(loginUser.fulfilled, (state, action: PayloadAction<{ jwt: string; refreshToken: string }>) => {
- state.status = "successful";
- state.jwt = action.payload.jwt;
- state.refreshToken = action.payload.refreshToken;
- axios.defaults.headers.common['Authorization'] = `Bearer ${action.payload.jwt}`;
- localStorage.setItem("jwt", action.payload.jwt);
- localStorage.setItem("refreshToken", action.payload.refreshToken);
- });
- builder.addCase(loginUser.rejected, (state, action: PayloadAction) => {
- state.status = "failed";
- state.error = action.payload;
- });
+export const loginUser = createAsyncThunk(
+ 'auth/login',
+ async (
+ { username, password }: { username: string; password: string },
+ { rejectWithValue },
+ ) => {
+ try {
+ const response = await axios.post('/authentication/login', {
+ username,
+ password,
+ });
+ return response.data;
+ } catch (err: any) {
+ return rejectWithValue(
+ err.response?.data?.message || 'Login failed',
+ );
+ }
+ },
+);
- // Обновление токена
- builder.addCase(refreshToken.pending, (state) => {
- state.status = "loading";
- state.error = null;
- });
- builder.addCase(refreshToken.fulfilled, (state, action: PayloadAction<{ username: string }>) => {
- state.status = "successful";
- state.username = action.payload.username;
- });
- builder.addCase(refreshToken.rejected, (state, action: PayloadAction) => {
- state.status = "failed";
- state.error = action.payload;
- });
+export const refreshToken = createAsyncThunk(
+ 'auth/refresh',
+ async ({ refreshToken }: { refreshToken: string }, { rejectWithValue }) => {
+ try {
+ const response = await axios.post('/authentication/refresh', {
+ refreshToken,
+ });
+ return response.data;
+ } catch (err: any) {
+ return rejectWithValue(
+ err.response?.data?.message || 'Refresh token failed',
+ );
+ }
+ },
+);
- // Получение информации о пользователе
- builder.addCase(fetchWhoAmI.pending, (state) => {
- state.status = "loading";
- state.error = null;
- });
- builder.addCase(fetchWhoAmI.fulfilled, (state, action: PayloadAction<{ username: string }>) => {
- state.status = "successful";
- state.username = action.payload.username;
- });
- builder.addCase(fetchWhoAmI.rejected, (state, action: PayloadAction) => {
- state.status = "failed";
- state.error = action.payload;
- });
+export const fetchWhoAmI = createAsyncThunk(
+ 'auth/whoami',
+ async (_, { rejectWithValue }) => {
+ try {
+ const response = await axios.get('/authentication/whoami');
+ return response.data;
+ } catch (err: any) {
+ return rejectWithValue(
+ err.response?.data?.message || 'Failed to fetch user info',
+ );
+ }
+ },
+);
- // Загрузка токенов из localStorage
- builder.addCase(loadTokensFromLocalStorage.fulfilled, (state, action: PayloadAction<{ jwt: string | null; refreshToken: string | null }>) => {
- state.jwt = action.payload.jwt;
- state.refreshToken = action.payload.refreshToken;
- if (action.payload.jwt) {
- axios.defaults.headers.common['Authorization'] = `Bearer ${action.payload.jwt}`;
- }
- });
- },
+// 🔹 Slice
+const authSlice = createSlice({
+ name: 'auth',
+ initialState,
+ reducers: {
+ logout: (state) => {
+ state.jwt = null;
+ state.refreshToken = null;
+ state.username = null;
+ state.email = null;
+ state.id = null;
+ state.status = 'idle';
+ state.error = null;
+ localStorage.removeItem('jwt');
+ localStorage.removeItem('refreshToken');
+ delete axios.defaults.headers.common['Authorization'];
+ },
+ },
+ extraReducers: (builder) => {
+ // ----------------- Register -----------------
+ builder.addCase(registerUser.pending, (state) => {
+ state.status = 'loading';
+ state.error = null;
+ });
+ builder.addCase(registerUser.fulfilled, (state, action) => {
+ state.status = 'successful';
+ state.jwt = action.payload.jwt;
+ state.refreshToken = action.payload.refreshToken;
+
+ const decoded = decodeJwt(action.payload.jwt);
+ state.username =
+ decoded[
+ 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name'
+ ] || null;
+ state.email =
+ decoded[
+ 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress'
+ ] || null;
+ state.id =
+ decoded[
+ 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier'
+ ] || null;
+
+ axios.defaults.headers.common[
+ 'Authorization'
+ ] = `Bearer ${action.payload.jwt}`;
+ localStorage.setItem('jwt', action.payload.jwt);
+ localStorage.setItem('refreshToken', action.payload.refreshToken);
+ });
+ builder.addCase(registerUser.rejected, (state, action) => {
+ state.status = 'failed';
+ state.error = action.payload as string;
+ });
+
+ // ----------------- Login -----------------
+ builder.addCase(loginUser.pending, (state) => {
+ state.status = 'loading';
+ state.error = null;
+ });
+ builder.addCase(loginUser.fulfilled, (state, action) => {
+ state.status = 'successful';
+ state.jwt = action.payload.jwt;
+ state.refreshToken = action.payload.refreshToken;
+
+ const decoded = decodeJwt(action.payload.jwt);
+ state.username =
+ decoded[
+ 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name'
+ ] || null;
+ state.email =
+ decoded[
+ 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress'
+ ] || null;
+ state.id =
+ decoded[
+ 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier'
+ ] || null;
+
+ axios.defaults.headers.common[
+ 'Authorization'
+ ] = `Bearer ${action.payload.jwt}`;
+ localStorage.setItem('jwt', action.payload.jwt);
+ localStorage.setItem('refreshToken', action.payload.refreshToken);
+ });
+ builder.addCase(loginUser.rejected, (state, action) => {
+ state.status = 'failed';
+ state.error = action.payload as string;
+ });
+
+ // ----------------- Refresh -----------------
+ builder.addCase(refreshToken.pending, (state) => {
+ state.status = 'loading';
+ state.error = null;
+ });
+ builder.addCase(refreshToken.fulfilled, (state, action) => {
+ state.status = 'successful';
+ state.jwt = action.payload.jwt;
+ state.refreshToken = action.payload.refreshToken;
+
+ const decoded = decodeJwt(action.payload.jwt);
+ state.username =
+ decoded[
+ 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name'
+ ] || null;
+ state.email =
+ decoded[
+ 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress'
+ ] || null;
+ state.id =
+ decoded[
+ 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier'
+ ] || null;
+
+ axios.defaults.headers.common[
+ 'Authorization'
+ ] = `Bearer ${action.payload.jwt}`;
+ localStorage.setItem('jwt', action.payload.jwt);
+ localStorage.setItem('refreshToken', action.payload.refreshToken);
+ });
+ builder.addCase(refreshToken.rejected, (state, action) => {
+ state.status = 'failed';
+ state.error = action.payload as string;
+ });
+
+ // ----------------- WhoAmI -----------------
+ builder.addCase(fetchWhoAmI.pending, (state) => {
+ state.status = 'loading';
+ state.error = null;
+ });
+ builder.addCase(fetchWhoAmI.fulfilled, (state, action) => {
+ state.status = 'successful';
+ state.username = action.payload.username;
+ });
+ builder.addCase(fetchWhoAmI.rejected, (state, action) => {
+ state.status = 'failed';
+ state.error = action.payload as string;
+ });
+ },
});
export const { logout } = authSlice.actions;
diff --git a/src/redux/slices/contests.ts b/src/redux/slices/contests.ts
new file mode 100644
index 0000000..a5302ec
--- /dev/null
+++ b/src/redux/slices/contests.ts
@@ -0,0 +1,245 @@
+import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
+import axios from '../../axios';
+
+// =====================
+// Типы
+// =====================
+
+export interface Mission {
+ id: number;
+ authorId: number;
+ name: string;
+ difficulty: number;
+ tags: string[];
+ createdAt: string;
+ updatedAt: string;
+ timeLimitMilliseconds: number;
+ memoryLimitBytes: number;
+ statements: null;
+}
+
+export interface Member {
+ userId: number;
+ username: string;
+ role: 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[];
+}
+
+interface ContestsResponse {
+ hasNextPage: boolean;
+ contests: Contest[];
+}
+
+export interface CreateContestBody {
+ name?: string | null;
+ description?: string | null;
+ 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;
+}
+
+// =====================
+// Состояние
+// =====================
+
+type Status = 'idle' | 'loading' | 'successful' | 'failed';
+
+interface ContestsState {
+ contests: Contest[];
+ selectedContest: Contest | null;
+ hasNextPage: boolean;
+ statuses: {
+ fetchList: Status;
+ fetchById: Status;
+ create: Status;
+ };
+ error: string | null;
+}
+
+const initialState: ContestsState = {
+ contests: [],
+ selectedContest: null,
+ hasNextPage: false,
+ statuses: {
+ fetchList: 'idle',
+ fetchById: 'idle',
+ create: 'idle',
+ },
+ error: null,
+};
+
+// =====================
+// Async Thunks
+// =====================
+
+export const fetchContests = createAsyncThunk(
+ 'contests/fetchAll',
+ async (
+ params: {
+ page?: number;
+ pageSize?: number;
+ groupId?: number | null;
+ } = {},
+ { rejectWithValue },
+ ) => {
+ try {
+ const { page = 0, pageSize = 10, groupId } = params;
+ const response = await axios.get('/contests', {
+ params: { page, pageSize, groupId },
+ });
+ return response.data;
+ } catch (err: any) {
+ return rejectWithValue(
+ err.response?.data?.message || 'Failed to fetch contests',
+ );
+ }
+ },
+);
+
+export const fetchContestById = createAsyncThunk(
+ 'contests/fetchById',
+ async (id: number, { rejectWithValue }) => {
+ try {
+ const response = await axios.get(`/contests/${id}`);
+ return response.data;
+ } catch (err: any) {
+ return rejectWithValue(
+ err.response?.data?.message || 'Failed to fetch contest',
+ );
+ }
+ },
+);
+
+export const createContest = createAsyncThunk(
+ 'contests/create',
+ async (contestData: CreateContestBody, { rejectWithValue }) => {
+ try {
+ const response = await axios.post(
+ '/contests',
+ contestData,
+ );
+ return response.data;
+ } catch (err: any) {
+ return rejectWithValue(
+ err.response?.data?.message || 'Failed to create contest',
+ );
+ }
+ },
+);
+
+// =====================
+// Slice
+// =====================
+
+const contestsSlice = createSlice({
+ name: 'contests',
+ initialState,
+ reducers: {
+ clearSelectedContest: (state) => {
+ state.selectedContest = null;
+ },
+ setContestStatus: (
+ state,
+ action: PayloadAction<{
+ key: keyof ContestsState['statuses'];
+ status: Status;
+ }>,
+ ) => {
+ state.statuses[action.payload.key] = action.payload.status;
+ },
+ },
+ extraReducers: (builder) => {
+ // fetchContests
+ builder.addCase(fetchContests.pending, (state) => {
+ state.statuses.fetchList = 'loading';
+ state.error = null;
+ });
+ 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;
+ },
+ );
+
+ // fetchContestById
+ builder.addCase(fetchContestById.pending, (state) => {
+ state.statuses.fetchById = 'loading';
+ state.error = null;
+ });
+ 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;
+ },
+ );
+
+ // createContest
+ builder.addCase(createContest.pending, (state) => {
+ state.statuses.create = 'loading';
+ state.error = null;
+ });
+ builder.addCase(
+ createContest.fulfilled,
+ (state, action: PayloadAction) => {
+ state.statuses.create = 'successful';
+ state.contests.unshift(action.payload);
+ },
+ );
+ builder.addCase(
+ createContest.rejected,
+ (state, action: PayloadAction) => {
+ state.statuses.create = 'failed';
+ state.error = action.payload;
+ },
+ );
+ },
+});
+
+// =====================
+// Экспорты
+// =====================
+
+export const { clearSelectedContest, setContestStatus } = contestsSlice.actions;
+export const contestsReducer = contestsSlice.reducer;
diff --git a/src/redux/slices/groups.ts b/src/redux/slices/groups.ts
new file mode 100644
index 0000000..e9c586d
--- /dev/null
+++ b/src/redux/slices/groups.ts
@@ -0,0 +1,350 @@
+import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
+import axios from '../../axios';
+
+// ─── Типы ────────────────────────────────────────────
+
+type Status = 'idle' | 'loading' | 'successful' | 'failed';
+
+export interface GroupMember {
+ userId: number;
+ username: string;
+ role: string;
+}
+
+export interface Group {
+ id: number;
+ name: string;
+ description: string;
+ members: GroupMember[];
+ contests: any[];
+}
+
+interface GroupsState {
+ groups: Group[];
+ currentGroup: Group | null;
+ statuses: {
+ create: Status;
+ update: Status;
+ delete: Status;
+ fetchMy: Status;
+ fetchById: Status;
+ addMember: Status;
+ removeMember: Status;
+ };
+ error: string | null;
+}
+
+const initialState: GroupsState = {
+ groups: [],
+ currentGroup: null,
+ statuses: {
+ create: 'idle',
+ update: 'idle',
+ delete: 'idle',
+ fetchMy: 'idle',
+ fetchById: 'idle',
+ addMember: 'idle',
+ removeMember: 'idle',
+ },
+ error: null,
+};
+
+// ─── Async Thunks ─────────────────────────────────────
+
+// POST /groups
+export const createGroup = createAsyncThunk(
+ 'groups/createGroup',
+ async (
+ { name, description }: { name: string; description: string },
+ { rejectWithValue },
+ ) => {
+ try {
+ const response = await axios.post('/groups', { name, description });
+ return response.data as Group;
+ } catch (err: any) {
+ return rejectWithValue(
+ err.response?.data?.message || 'Ошибка при создании группы',
+ );
+ }
+ },
+);
+
+// PUT /groups/{groupId}
+export const updateGroup = createAsyncThunk(
+ 'groups/updateGroup',
+ async (
+ {
+ groupId,
+ name,
+ description,
+ }: { groupId: number; name: string; description: string },
+ { rejectWithValue },
+ ) => {
+ try {
+ const response = await axios.put(`/groups/${groupId}`, {
+ name,
+ description,
+ });
+ return response.data as Group;
+ } catch (err: any) {
+ return rejectWithValue(
+ err.response?.data?.message || 'Ошибка при обновлении группы',
+ );
+ }
+ },
+);
+
+// DELETE /groups/{groupId}
+export const deleteGroup = createAsyncThunk(
+ 'groups/deleteGroup',
+ async (groupId: number, { rejectWithValue }) => {
+ try {
+ await axios.delete(`/groups/${groupId}`);
+ return groupId;
+ } catch (err: any) {
+ return rejectWithValue(
+ err.response?.data?.message || 'Ошибка при удалении группы',
+ );
+ }
+ },
+);
+
+// GET /groups/my
+export const fetchMyGroups = createAsyncThunk(
+ 'groups/fetchMyGroups',
+ async (_, { rejectWithValue }) => {
+ try {
+ const response = await axios.get('/groups/my');
+ return response.data.groups as Group[];
+ } catch (err: any) {
+ return rejectWithValue(
+ err.response?.data?.message || 'Ошибка при получении групп',
+ );
+ }
+ },
+);
+
+// GET /groups/{groupId}
+export const fetchGroupById = createAsyncThunk(
+ 'groups/fetchGroupById',
+ async (groupId: number, { rejectWithValue }) => {
+ try {
+ const response = await axios.get(`/groups/${groupId}`);
+ return response.data as Group;
+ } catch (err: any) {
+ return rejectWithValue(
+ err.response?.data?.message || 'Ошибка при получении группы',
+ );
+ }
+ },
+);
+
+// POST /groups/members
+export const addGroupMember = createAsyncThunk(
+ 'groups/addGroupMember',
+ async (
+ { userId, role }: { userId: number; role: string },
+ { rejectWithValue },
+ ) => {
+ try {
+ await axios.post('/groups/members', { userId, role });
+ return { userId, role };
+ } catch (err: any) {
+ return rejectWithValue(
+ err.response?.data?.message ||
+ 'Ошибка при добавлении участника',
+ );
+ }
+ },
+);
+
+// DELETE /groups/{groupId}/members/{memberId}
+export const removeGroupMember = createAsyncThunk(
+ 'groups/removeGroupMember',
+ async (
+ { groupId, memberId }: { groupId: number; memberId: number },
+ { rejectWithValue },
+ ) => {
+ try {
+ await axios.delete(`/groups/${groupId}/members/${memberId}`);
+ return { groupId, memberId };
+ } catch (err: any) {
+ return rejectWithValue(
+ err.response?.data?.message || 'Ошибка при удалении участника',
+ );
+ }
+ },
+);
+
+// ─── Slice ────────────────────────────────────────────
+
+const groupsSlice = createSlice({
+ name: 'groups',
+ initialState,
+ reducers: {
+ clearCurrentGroup: (state) => {
+ state.currentGroup = null;
+ },
+ },
+ 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 ───
+ builder.addCase(fetchMyGroups.pending, (state) => {
+ state.statuses.fetchMy = 'loading';
+ state.error = null;
+ });
+ 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;
+ },
+ );
+
+ // ─── FETCH GROUP BY ID ───
+ builder.addCase(fetchGroupById.pending, (state) => {
+ state.statuses.fetchById = 'loading';
+ state.error = null;
+ });
+ 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;
+ },
+ );
+
+ // ─── ADD MEMBER ───
+ builder.addCase(addGroupMember.pending, (state) => {
+ state.statuses.addMember = 'loading';
+ state.error = null;
+ });
+ builder.addCase(addGroupMember.fulfilled, (state) => {
+ state.statuses.addMember = 'successful';
+ });
+ builder.addCase(
+ addGroupMember.rejected,
+ (state, action: PayloadAction) => {
+ state.statuses.addMember = 'failed';
+ state.error = action.payload;
+ },
+ );
+
+ // ─── REMOVE MEMBER ───
+ builder.addCase(removeGroupMember.pending, (state) => {
+ state.statuses.removeMember = 'loading';
+ state.error = null;
+ });
+ builder.addCase(
+ removeGroupMember.fulfilled,
+ (
+ state,
+ action: PayloadAction<{ groupId: number; memberId: number }>,
+ ) => {
+ state.statuses.removeMember = 'successful';
+ if (
+ state.currentGroup &&
+ state.currentGroup.id === action.payload.groupId
+ ) {
+ state.currentGroup.members =
+ state.currentGroup.members.filter(
+ (m) => m.userId !== action.payload.memberId,
+ );
+ }
+ },
+ );
+ builder.addCase(
+ removeGroupMember.rejected,
+ (state, action: PayloadAction) => {
+ state.statuses.removeMember = 'failed';
+ state.error = action.payload;
+ },
+ );
+ },
+});
+
+export const { clearCurrentGroup } = groupsSlice.actions;
+export const groupsReducer = groupsSlice.reducer;
diff --git a/src/redux/slices/missions.ts b/src/redux/slices/missions.ts
index ee1bcb8..93f24eb 100644
--- a/src/redux/slices/missions.ts
+++ b/src/redux/slices/missions.ts
@@ -1,146 +1,215 @@
-import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit";
-import axios from "../../axios";
+import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
+import axios from '../../axios';
+
+// ─── Типы ────────────────────────────────────────────
+
+type Status = 'idle' | 'loading' | 'successful' | 'failed';
-// Типы данных
interface Statement {
- id: number;
- language: string;
- statementTexts: Record;
- mediaFiles?: { id: number; fileName: string; mediaUrl: string }[];
+ id: number;
+ language: string;
+ statementTexts: Record;
+ mediaFiles?: { id: number; fileName: string; mediaUrl: string }[];
}
-interface Mission {
- id: number;
- authorId: number;
- name: string;
- difficulty: number;
- tags: string[];
- createdAt: string;
- updatedAt: string;
- statements?: Statement[];
+export interface Mission {
+ id: number;
+ authorId: number;
+ name: string;
+ difficulty: number;
+ tags: string[];
+ createdAt: string;
+ updatedAt: string;
+ statements?: Statement[];
}
interface MissionsState {
- missions: Mission[];
- currentMission: Mission | null;
- hasNextPage: boolean;
- status: "idle" | "loading" | "successful" | "failed";
- error: string | null;
+ missions: Mission[];
+ currentMission: Mission | null;
+ hasNextPage: boolean;
+ statuses: {
+ fetchList: Status;
+ fetchById: Status;
+ upload: Status;
+ };
+ error: string | null;
}
-// Инициализация состояния
+// ─── Инициализация состояния ──────────────────────────
+
const initialState: MissionsState = {
- missions: [],
- currentMission: null,
- hasNextPage: false,
- status: "idle",
- error: null,
+ missions: [],
+ currentMission: null,
+ hasNextPage: false,
+ statuses: {
+ fetchList: 'idle',
+ fetchById: 'idle',
+ upload: 'idle',
+ },
+ error: null,
};
-// AsyncThunk: Получение списка миссий
+// ─── Async Thunks ─────────────────────────────────────
+
+// GET /missions
export const fetchMissions = createAsyncThunk(
- "missions/fetchMissions",
- async (
- { page = 0, pageSize = 10, tags = [] }: { page?: number; pageSize?: number; tags?: string[] },
- { rejectWithValue }
- ) => {
- try {
- const params: any = { page, pageSize };
- if (tags) params.tags = tags;
- const response = await axios.get("/missions", { params });
- return response.data; // { hasNextPage, missions }
- } catch (err: any) {
- return rejectWithValue(err.response?.data?.message || "Failed to fetch missions");
- }
- }
+ 'missions/fetchMissions',
+ async (
+ {
+ page = 0,
+ pageSize = 10,
+ tags = [],
+ }: { page?: number; pageSize?: number; tags?: string[] },
+ { rejectWithValue },
+ ) => {
+ try {
+ const params: any = { page, pageSize };
+ if (tags.length) params.tags = tags;
+ const response = await axios.get('/missions', { params });
+ return response.data; // { missions, hasNextPage }
+ } catch (err: any) {
+ return rejectWithValue(
+ err.response?.data?.message || 'Ошибка при получении миссий',
+ );
+ }
+ },
);
-// AsyncThunk: Получение миссии по id
+// GET /missions/{id}
export const fetchMissionById = createAsyncThunk(
- "missions/fetchMissionById",
- async (id: number, { rejectWithValue }) => {
- try {
- const response = await axios.get(`/missions/${id}`);
- return response.data; // Mission
- } catch (err: any) {
- return rejectWithValue(err.response?.data?.message || "Failed to fetch mission");
- }
- }
+ 'missions/fetchMissionById',
+ async (id: number, { rejectWithValue }) => {
+ try {
+ const response = await axios.get(`/missions/${id}`);
+ return response.data; // Mission
+ } catch (err: any) {
+ return rejectWithValue(
+ err.response?.data?.message || 'Ошибка при получении миссии',
+ );
+ }
+ },
);
-// AsyncThunk: Загрузка миссии
+// POST /missions/upload
export const uploadMission = createAsyncThunk(
- "missions/uploadMission",
- async (
- { file, name, difficulty, tags }: { file: File; name: string; difficulty: number; tags: string[] },
- { rejectWithValue }
- ) => {
- try {
- const formData = new FormData();
- formData.append("MissionFile", file);
- formData.append("Name", name);
- formData.append("Difficulty", difficulty.toString());
- tags.forEach(tag => formData.append("Tags", tag));
+ 'missions/uploadMission',
+ async (
+ {
+ file,
+ name,
+ difficulty,
+ tags,
+ }: { file: File; name: string; difficulty: number; tags: string[] },
+ { rejectWithValue },
+ ) => {
+ try {
+ const formData = new FormData();
+ formData.append('MissionFile', file);
+ formData.append('Name', name);
+ formData.append('Difficulty', difficulty.toString());
+ tags.forEach((tag) => formData.append('Tags', tag));
- const response = await axios.post("/missions/upload", formData, {
- headers: { "Content-Type": "multipart/form-data" },
- });
- return response.data; // Mission
- } catch (err: any) {
- return rejectWithValue(err.response?.data?.message || "Failed to upload mission");
- }
- }
+ const response = await axios.post('/missions/upload', formData, {
+ headers: { 'Content-Type': 'multipart/form-data' },
+ });
+ return response.data; // Mission
+ } catch (err: any) {
+ return rejectWithValue(
+ err.response?.data?.message || 'Ошибка при загрузке миссии',
+ );
+ }
+ },
);
-// Slice
+// ─── Slice ────────────────────────────────────────────
+
const missionsSlice = createSlice({
- name: "missions",
- initialState,
- reducers: {},
- extraReducers: (builder) => {
- // fetchMissions
- builder.addCase(fetchMissions.pending, (state) => {
- state.status = "loading";
- state.error = null;
- });
- builder.addCase(fetchMissions.fulfilled, (state, action: PayloadAction<{ missions: Mission[]; hasNextPage: boolean }>) => {
- state.status = "successful";
- state.missions = action.payload.missions;
- state.hasNextPage = action.payload.hasNextPage;
- });
- builder.addCase(fetchMissions.rejected, (state, action: PayloadAction) => {
- state.status = "failed";
- state.error = action.payload;
- });
+ name: 'missions',
+ initialState,
+ reducers: {
+ clearCurrentMission: (state) => {
+ state.currentMission = null;
+ },
+ setMissionsStatus: (
+ state,
+ action: PayloadAction<{
+ key: keyof MissionsState['statuses'];
+ status: Status;
+ }>,
+ ) => {
+ const { key, status } = action.payload;
+ state.statuses[key] = status;
+ },
+ },
+ extraReducers: (builder) => {
+ // ─── FETCH MISSIONS ───
+ builder.addCase(fetchMissions.pending, (state) => {
+ state.statuses.fetchList = 'loading';
+ state.error = null;
+ });
+ builder.addCase(
+ fetchMissions.fulfilled,
+ (
+ state,
+ action: PayloadAction<{
+ missions: Mission[];
+ hasNextPage: boolean;
+ }>,
+ ) => {
+ state.statuses.fetchList = 'successful';
+ state.missions = action.payload.missions;
+ state.hasNextPage = action.payload.hasNextPage;
+ },
+ );
+ builder.addCase(
+ fetchMissions.rejected,
+ (state, action: PayloadAction) => {
+ state.statuses.fetchList = 'failed';
+ state.error = action.payload;
+ },
+ );
- // fetchMissionById
- builder.addCase(fetchMissionById.pending, (state) => {
- state.status = "loading";
- state.error = null;
- });
- builder.addCase(fetchMissionById.fulfilled, (state, action: PayloadAction) => {
- state.status = "successful";
- state.currentMission = action.payload;
- });
- builder.addCase(fetchMissionById.rejected, (state, action: PayloadAction) => {
- state.status = "failed";
- state.error = action.payload;
- });
+ // ─── FETCH MISSION BY ID ───
+ builder.addCase(fetchMissionById.pending, (state) => {
+ state.statuses.fetchById = 'loading';
+ state.error = null;
+ });
+ builder.addCase(
+ fetchMissionById.fulfilled,
+ (state, action: PayloadAction) => {
+ state.statuses.fetchById = 'successful';
+ state.currentMission = action.payload;
+ },
+ );
+ builder.addCase(
+ fetchMissionById.rejected,
+ (state, action: PayloadAction) => {
+ state.statuses.fetchById = 'failed';
+ state.error = action.payload;
+ },
+ );
- // uploadMission
- builder.addCase(uploadMission.pending, (state) => {
- state.status = "loading";
- state.error = null;
- });
- builder.addCase(uploadMission.fulfilled, (state, action: PayloadAction) => {
- state.status = "successful";
- state.missions.unshift(action.payload); // Добавляем новую миссию в начало списка
- });
- builder.addCase(uploadMission.rejected, (state, action: PayloadAction) => {
- state.status = "failed";
- state.error = action.payload;
- });
- },
+ // ─── UPLOAD MISSION ───
+ builder.addCase(uploadMission.pending, (state) => {
+ state.statuses.upload = 'loading';
+ state.error = null;
+ });
+ builder.addCase(
+ uploadMission.fulfilled,
+ (state, action: PayloadAction) => {
+ state.statuses.upload = 'successful';
+ state.missions.unshift(action.payload);
+ },
+ );
+ builder.addCase(
+ uploadMission.rejected,
+ (state, action: PayloadAction) => {
+ state.statuses.upload = 'failed';
+ state.error = action.payload;
+ },
+ );
+ },
});
+export const { clearCurrentMission, setMissionsStatus } = missionsSlice.actions;
export const missionsReducer = missionsSlice.reducer;
diff --git a/src/redux/slices/store.ts b/src/redux/slices/store.ts
index d36e857..bd54ba6 100644
--- a/src/redux/slices/store.ts
+++ b/src/redux/slices/store.ts
@@ -1,30 +1,38 @@
-import { createSlice, PayloadAction} from "@reduxjs/toolkit";
+import { createSlice, PayloadAction } from '@reduxjs/toolkit';
// Типы данных
interface StorState {
- menu: {
- activePage: string;
- }
+ menu: {
+ activePage: string;
+ activeProfilePage: string;
+ };
}
// Инициализация состояния
const initialState: StorState = {
menu: {
- activePage: "",
- }
+ activePage: '',
+ activeProfilePage: '',
+ },
};
-
// Slice
const storeSlice = createSlice({
- name: "store",
- initialState,
- reducers: {
- setMenuActivePage: (state, activePage: PayloadAction) => {
- state.menu.activePage = activePage.payload;
+ name: 'store',
+ initialState,
+ reducers: {
+ setMenuActivePage: (state, activePage: PayloadAction) => {
+ state.menu.activePage = activePage.payload;
+ },
+ setMenuActiveProfilePage: (
+ state,
+ activeProfilePage: PayloadAction,
+ ) => {
+ state.menu.activeProfilePage = activeProfilePage.payload;
+ },
},
- },
});
-export const { setMenuActivePage } = storeSlice.actions;
+export const { setMenuActivePage, setMenuActiveProfilePage } =
+ storeSlice.actions;
export const storeReducer = storeSlice.reducer;
diff --git a/src/redux/slices/submit.ts b/src/redux/slices/submit.ts
index fbe3265..b70efe8 100644
--- a/src/redux/slices/submit.ts
+++ b/src/redux/slices/submit.ts
@@ -1,184 +1,224 @@
-import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit";
-import axios from "../../axios";
+import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
+import axios from '../../axios';
// Типы данных
export interface Submit {
- id?: number;
- missionId: number;
- language: string;
- languageVersion: string;
- sourceCode: string;
- contestId: number | null;
+ id?: number;
+ missionId: number;
+ language: string;
+ languageVersion: string;
+ sourceCode: string;
+ contestId: number | null;
}
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;
+ 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 MissionSubmit {
- id: number;
- userId: number;
- solution: Solution;
- contestId: number | null;
- contestName: string | null;
- sourceType: string;
+ id: number;
+ userId: number;
+ solution: Solution;
+ contestId: number | null;
+ contestName: string | null;
+ sourceType: string;
}
interface SubmitState {
- submits: Submit[];
- submitsById: Record; // ✅ добавлено
- currentSubmit?: Submit;
- status: "idle" | "loading" | "successful" | "failed";
- error: string | null;
+ submits: Submit[];
+ submitsById: Record; // ✅ добавлено
+ currentSubmit?: Submit;
+ status: 'idle' | 'loading' | 'successful' | 'failed';
+ error: string | null;
}
// Начальное состояние
const initialState: SubmitState = {
- submits: [],
- submitsById: {}, // ✅ инициализация
- currentSubmit: undefined,
- status: "idle",
- error: null,
+ submits: [],
+ submitsById: {}, // ✅ инициализация
+ currentSubmit: undefined,
+ status: 'idle',
+ error: null,
};
// AsyncThunk: Отправка решения
export const submitMission = createAsyncThunk(
- "submit/submitMission",
- async (submitData: Submit, { rejectWithValue }) => {
- try {
- const response = await axios.post("/submits", submitData);
- return response.data;
- } catch (err: any) {
- return rejectWithValue(err.response?.data?.message || "Submit failed");
- }
- }
+ 'submit/submitMission',
+ async (submitData: Submit, { rejectWithValue }) => {
+ try {
+ const response = await axios.post('/submits', submitData);
+ return response.data;
+ } catch (err: any) {
+ return rejectWithValue(
+ err.response?.data?.message || 'Submit failed',
+ );
+ }
+ },
);
// AsyncThunk: Получить все свои отправки
export const fetchMySubmits = createAsyncThunk(
- "submit/fetchMySubmits",
- async (_, { rejectWithValue }) => {
- try {
- const response = await axios.get("/submits/my");
- return response.data as Submit[];
- } catch (err: any) {
- return rejectWithValue(err.response?.data?.message || "Failed to fetch submits");
- }
- }
+ 'submit/fetchMySubmits',
+ async (_, { rejectWithValue }) => {
+ try {
+ const response = await axios.get('/submits/my');
+ return response.data as Submit[];
+ } catch (err: any) {
+ return rejectWithValue(
+ err.response?.data?.message || 'Failed to fetch submits',
+ );
+ }
+ },
);
// AsyncThunk: Получить конкретную отправку по ID
export const fetchSubmitById = createAsyncThunk(
- "submit/fetchSubmitById",
- async (id: number, { rejectWithValue }) => {
- try {
- const response = await axios.get(`/submits/${id}`);
- return response.data as Submit;
- } catch (err: any) {
- return rejectWithValue(err.response?.data?.message || "Failed to fetch submit");
- }
- }
+ 'submit/fetchSubmitById',
+ async (id: number, { rejectWithValue }) => {
+ try {
+ const response = await axios.get(`/submits/${id}`);
+ return response.data as Submit;
+ } catch (err: any) {
+ return rejectWithValue(
+ err.response?.data?.message || 'Failed to fetch submit',
+ );
+ }
+ },
);
// ✅ AsyncThunk: Получить отправки для конкретной миссии (новая структура)
export const fetchMySubmitsByMission = createAsyncThunk(
- "submit/fetchMySubmitsByMission",
- async (missionId: number, { rejectWithValue }) => {
- try {
- const response = await axios.get(`/submits/my/mission/${missionId}`);
- return { missionId, data: response.data as MissionSubmit[] };
- } catch (err: any) {
- return rejectWithValue(err.response?.data?.message || "Failed to fetch mission submits");
- }
- }
+ 'submit/fetchMySubmitsByMission',
+ async (missionId: number, { rejectWithValue }) => {
+ try {
+ const response = await axios.get(
+ `/submits/my/mission/${missionId}`,
+ );
+ return { missionId, data: response.data as MissionSubmit[] };
+ } catch (err: any) {
+ return rejectWithValue(
+ err.response?.data?.message ||
+ 'Failed to fetch mission submits',
+ );
+ }
+ },
);
// Slice
const submitSlice = createSlice({
- name: "submit",
- initialState,
- reducers: {
- clearCurrentSubmit: (state) => {
- state.currentSubmit = undefined;
- state.status = "idle";
- state.error = null;
+ name: 'submit',
+ initialState,
+ reducers: {
+ clearCurrentSubmit: (state) => {
+ state.currentSubmit = undefined;
+ state.status = 'idle';
+ state.error = null;
+ },
+ clearSubmitsByMission: (state, action: PayloadAction) => {
+ delete state.submitsById[action.payload];
+ },
},
- clearSubmitsByMission: (state, action: PayloadAction) => {
- delete state.submitsById[action.payload];
+ extraReducers: (builder) => {
+ // Отправка решения
+ builder.addCase(submitMission.pending, (state) => {
+ state.status = 'loading';
+ state.error = null;
+ });
+ builder.addCase(
+ submitMission.fulfilled,
+ (state, action: PayloadAction) => {
+ state.status = 'successful';
+ state.submits.push(action.payload);
+ },
+ );
+ builder.addCase(
+ submitMission.rejected,
+ (state, action: PayloadAction) => {
+ state.status = 'failed';
+ state.error = action.payload;
+ },
+ );
+
+ // Получить все свои отправки
+ builder.addCase(fetchMySubmits.pending, (state) => {
+ state.status = 'loading';
+ state.error = null;
+ });
+ builder.addCase(
+ fetchMySubmits.fulfilled,
+ (state, action: PayloadAction) => {
+ state.status = 'successful';
+ state.submits = action.payload;
+ },
+ );
+ builder.addCase(
+ fetchMySubmits.rejected,
+ (state, action: PayloadAction) => {
+ state.status = 'failed';
+ state.error = action.payload;
+ },
+ );
+
+ // Получить отправку по ID
+ builder.addCase(fetchSubmitById.pending, (state) => {
+ state.status = 'loading';
+ state.error = null;
+ });
+ builder.addCase(
+ fetchSubmitById.fulfilled,
+ (state, action: PayloadAction) => {
+ state.status = 'successful';
+ state.currentSubmit = action.payload;
+ },
+ );
+ builder.addCase(
+ fetchSubmitById.rejected,
+ (state, action: PayloadAction) => {
+ state.status = 'failed';
+ state.error = action.payload;
+ },
+ );
+
+ // ✅ Получить отправки по миссии
+ builder.addCase(fetchMySubmitsByMission.pending, (state) => {
+ state.status = 'loading';
+ state.error = null;
+ });
+ builder.addCase(
+ fetchMySubmitsByMission.fulfilled,
+ (
+ state,
+ action: PayloadAction<{
+ missionId: number;
+ data: MissionSubmit[];
+ }>,
+ ) => {
+ state.status = 'successful';
+ state.submitsById[action.payload.missionId] =
+ action.payload.data;
+ },
+ );
+ builder.addCase(
+ fetchMySubmitsByMission.rejected,
+ (state, action: PayloadAction) => {
+ state.status = 'failed';
+ state.error = action.payload;
+ },
+ );
},
- },
- extraReducers: (builder) => {
- // Отправка решения
- builder.addCase(submitMission.pending, (state) => {
- state.status = "loading";
- state.error = null;
- });
- builder.addCase(submitMission.fulfilled, (state, action: PayloadAction) => {
- state.status = "successful";
- state.submits.push(action.payload);
- });
- builder.addCase(submitMission.rejected, (state, action: PayloadAction) => {
- state.status = "failed";
- state.error = action.payload;
- });
-
- // Получить все свои отправки
- builder.addCase(fetchMySubmits.pending, (state) => {
- state.status = "loading";
- state.error = null;
- });
- builder.addCase(fetchMySubmits.fulfilled, (state, action: PayloadAction) => {
- state.status = "successful";
- state.submits = action.payload;
- });
- builder.addCase(fetchMySubmits.rejected, (state, action: PayloadAction) => {
- state.status = "failed";
- state.error = action.payload;
- });
-
- // Получить отправку по ID
- builder.addCase(fetchSubmitById.pending, (state) => {
- state.status = "loading";
- state.error = null;
- });
- builder.addCase(fetchSubmitById.fulfilled, (state, action: PayloadAction) => {
- state.status = "successful";
- state.currentSubmit = action.payload;
- });
- builder.addCase(fetchSubmitById.rejected, (state, action: PayloadAction) => {
- state.status = "failed";
- state.error = action.payload;
- });
-
- // ✅ Получить отправки по миссии
- builder.addCase(fetchMySubmitsByMission.pending, (state) => {
- state.status = "loading";
- state.error = null;
- });
- builder.addCase(
- fetchMySubmitsByMission.fulfilled,
- (state, action: PayloadAction<{ missionId: number; data: MissionSubmit[] }>) => {
- state.status = "successful";
- state.submitsById[action.payload.missionId] = action.payload.data;
- }
- );
- builder.addCase(fetchMySubmitsByMission.rejected, (state, action: PayloadAction) => {
- state.status = "failed";
- state.error = action.payload;
- });
- },
});
-export const { clearCurrentSubmit, clearSubmitsByMission } = submitSlice.actions;
+export const { clearCurrentSubmit, clearSubmitsByMission } =
+ submitSlice.actions;
export const submitReducer = submitSlice.reducer;
diff --git a/src/redux/store.ts b/src/redux/store.ts
index 6ea89a8..edbe49c 100644
--- a/src/redux/store.ts
+++ b/src/redux/store.ts
@@ -1,9 +1,11 @@
-import { configureStore } from "@reduxjs/toolkit";
-import { authReducer } from "./slices/auth";
-import { storeReducer } from "./slices/store";
-import { missionsReducer } from "./slices/missions";
-import { submitReducer } from "./slices/submit";
-
+import { configureStore } from '@reduxjs/toolkit';
+import { authReducer } from './slices/auth';
+import { storeReducer } from './slices/store';
+import { missionsReducer } from './slices/missions';
+import { submitReducer } from './slices/submit';
+import { contestsReducer } from './slices/contests';
+import { groupsReducer } from './slices/groups';
+import { articlesReducer } from './slices/articles';
// использование
// import { useAppDispatch, useAppSelector } from '../redux/hooks';
@@ -13,15 +15,17 @@ import { submitReducer } from "./slices/submit";
// const dispatch = useAppDispatch();
// const user = useAppSelector((state) => state.user);
-
export const store = configureStore({
- reducer: {
- //user: userReducer,
- auth: authReducer,
- store: storeReducer,
- missions: missionsReducer,
- submin: submitReducer,
- },
+ reducer: {
+ //user: userReducer,
+ auth: authReducer,
+ store: storeReducer,
+ missions: missionsReducer,
+ submin: submitReducer,
+ contests: contestsReducer,
+ groups: groupsReducer,
+ articles: articlesReducer,
+ },
});
// тип состояния всего стора
diff --git a/src/styles/index.css b/src/styles/index.css
index 2063dc2..69b3072 100644
--- a/src/styles/index.css
+++ b/src/styles/index.css
@@ -2,116 +2,109 @@
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';
-@import "./latex-container.css";
+@import './latex-container.css';
* {
- -webkit-tap-highlight-color: transparent; /* Отключаем выделение синим при тапе на телефоне*/
- /* outline: 1px solid green; */
-}
-
+ -webkit-tap-highlight-color: transparent; /* Отключаем выделение синим при тапе на телефоне*/
+ /* outline: 1px solid green; */
+}
:root {
- color-scheme: light dark;
- width: 100%;
- height: 100svh;
- /* @apply bg-layout-background; */
- /* transition: all linear 200ms; */
+ color-scheme: light dark;
+ width: 100%;
+ height: 100svh;
+ /* @apply bg-layout-background; */
+ /* transition: all linear 200ms; */
- font-family: 'Source Code Pro', monospace;
-
- /* font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; */
- font-weight: 400;
- line-height: 1.5;
- background-color: var(--color-liquid-background);
- color: rgba(255, 255, 255, 0.87);
+ font-family: 'Source Code Pro', monospace;
+
+ /* font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; */
+ font-weight: 400;
+ line-height: 1.5;
+ background-color: var(--color-liquid-background);
+ color: rgba(255, 255, 255, 0.87);
+ overflow-x: hidden;
}
#root {
- width: 100%;
- height: 100vh;
+ width: 100%;
+ height: 100vh;
}
body {
- display: flex;
- justify-content: center;
- align-items: center;
- width: 100%;
- height: 100%;
- margin: 0;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 100%;
+ height: 100%;
+ margin: 0;
}
-
/* Общий контейнер полосы прокрутки */
.thin-scrollbar::-webkit-scrollbar {
- width: 4px; /* ширина вертикального */
+ width: 4px; /* ширина вертикального */
}
/* Трек (фон) */
.thin-scrollbar::-webkit-scrollbar-track {
- background: transparent;
+ background: transparent;
}
/* Ползунок (thumb) */
.thin-scrollbar::-webkit-scrollbar-thumb {
- background: var(--color-liquid-light);
- border-radius: 1000px;
- cursor: pointer;
+ background: var(--color-liquid-light);
+ border-radius: 1000px;
+ cursor: pointer;
}
-
/* Общий контейнер полосы прокрутки */
.medium-scrollbar::-webkit-scrollbar {
- width: 8px; /* ширина вертикального */
+ width: 8px; /* ширина вертикального */
}
/* Трек (фон) */
.medium-scrollbar::-webkit-scrollbar-track {
- background: transparent;
+ background: transparent;
}
/* Ползунок (thumb) */
.medium-scrollbar::-webkit-scrollbar-thumb {
- background: var(--color-liquid-light);
- border-radius: 1000px;
- cursor: pointer;
+ background: var(--color-liquid-light);
+ border-radius: 1000px;
+ cursor: pointer;
}
-
-
/* Общий контейнер полосы прокрутки */
.thin-dark-scrollbar::-webkit-scrollbar {
- width: 4px; /* ширина вертикального */
+ width: 4px; /* ширина вертикального */
}
/* Трек (фон) */
.thin-dark-scrollbar::-webkit-scrollbar-track {
- background: transparent;
+ background: transparent;
}
/* Ползунок (thumb) */
.thin-dark-scrollbar::-webkit-scrollbar-thumb {
- background: var(--color-liquid-lighter);
- border-radius: 1000px;
- cursor: pointer;
+ background: var(--color-liquid-lighter);
+ border-radius: 1000px;
+ cursor: pointer;
}
-
-
-
html {
scrollbar-gutter: stable;
padding-left: 8px;
}
html::-webkit-scrollbar {
- width: 8px; /* ширина вертикального */
+ width: 8px; /* ширина вертикального */
}
/* Трек (фон) */
html::-webkit-scrollbar-track {
- background: transparent;
+ background: transparent;
}
/* Ползунок (thumb) */
html::-webkit-scrollbar-thumb {
- background-color: var(--color-liquid-lighter);
- border-radius: 1000px;
- cursor: pointer;
-}
\ No newline at end of file
+ background-color: var(--color-liquid-lighter);
+ border-radius: 1000px;
+ cursor: pointer;
+}
diff --git a/src/styles/latex-container.css b/src/styles/latex-container.css
index 600b047..ddd0e74 100644
--- a/src/styles/latex-container.css
+++ b/src/styles/latex-container.css
@@ -1,26 +1,24 @@
-
.latex-container p {
- text-align: justify; /* выравнивание по ширине */
- text-justify: inter-word;
- margin-bottom: 0.8em; /* небольшой отступ между абзацами */
- line-height: 1.2;
- /* text-indent: 1em; */
+ text-align: justify; /* выравнивание по ширине */
+ text-justify: inter-word;
+ margin-bottom: 0.8em; /* небольшой отступ между абзацами */
+ line-height: 1.2;
+ /* text-indent: 1em; */
}
.latex-container ol {
- padding-left: 1.5em; /* отступ для нумерации */
- margin: 0.5em 0; /* небольшой отступ сверху и снизу */
- line-height: 1.5; /* удобный межстрочный интервал */
- font-family: "Inter", sans-serif;
- font-size: 1rem;
+ padding-left: 1.5em; /* отступ для нумерации */
+ margin: 0.5em 0; /* небольшой отступ сверху и снизу */
+ line-height: 1.5; /* удобный межстрочный интервал */
+ font-family: 'Inter', sans-serif;
+ font-size: 1rem;
}
.latex-container ol li {
- margin-bottom: 0.4em; /* расстояние между пунктами */
+ margin-bottom: 0.4em; /* расстояние между пунктами */
}
-.latex-container .section-title{
- font-size: 16px;
- font-weight: bold;
+.latex-container .section-title {
+ font-size: 16px;
+ font-weight: bold;
}
-
diff --git a/src/styles/palette/theme-dark.css b/src/styles/palette/theme-dark.css
index 435b197..061e3b2 100644
--- a/src/styles/palette/theme-dark.css
+++ b/src/styles/palette/theme-dark.css
@@ -1,16 +1,16 @@
@import 'tailwindcss/base';
@layer base {
- :root[data-theme~="dark"] {
- --color-liquid-brightmain: #00DBD9;
- --color-liquid-darkmain: #075867;
- --color-liquid-darker: #141515;
- --color-liquid-background: #202222;
- --color-liquid-lighter: #2A2E2F;
- --color-liquid-white: #EDF6F7;
- --color-liquid-red: #F13E5F;
- --color-liquid-green: #10BE59;
- --color-liquid-light: #576466;
- --color-liquid-orange: #FF951B;
- }
-}
\ No newline at end of file
+ :root[data-theme~='dark'] {
+ --color-liquid-brightmain: #00dbd9;
+ --color-liquid-darkmain: #075867;
+ --color-liquid-darker: #141515;
+ --color-liquid-background: #202222;
+ --color-liquid-lighter: #2a2e2f;
+ --color-liquid-white: #edf6f7;
+ --color-liquid-red: #f13e5f;
+ --color-liquid-green: #10be59;
+ --color-liquid-light: #576466;
+ --color-liquid-orange: #ff951b;
+ }
+}
diff --git a/src/styles/palette/theme-light.css b/src/styles/palette/theme-light.css
index 1d6bedc..77b2504 100644
--- a/src/styles/palette/theme-light.css
+++ b/src/styles/palette/theme-light.css
@@ -1,16 +1,16 @@
@import 'tailwindcss/base';
@layer base {
- :root {
- --color-liquid-brightmain: #00DBD9;
- --color-liquid-darkmain: #075867;
- --color-liquid-darker: #141515;
- --color-liquid-background: #202222;
- --color-liquid-lighter: #2A2E2F;
- --color-liquid-white: #EDF6F7;
- --color-liquid-red: #F13E5F;
- --color-liquid-green: #10BE59;
- --color-liquid-light: #576466;
- --color-liquid-orange: #FF951B;
- }
-}
\ No newline at end of file
+ :root {
+ --color-liquid-brightmain: #00dbd9;
+ --color-liquid-darkmain: #075867;
+ --color-liquid-darker: #141515;
+ --color-liquid-background: #202222;
+ --color-liquid-lighter: #2a2e2f;
+ --color-liquid-white: #edf6f7;
+ --color-liquid-red: #f13e5f;
+ --color-liquid-green: #10be59;
+ --color-liquid-light: #576466;
+ --color-liquid-orange: #ff951b;
+ }
+}
diff --git a/src/views/article/Header.tsx b/src/views/article/Header.tsx
new file mode 100644
index 0000000..dad0203
--- /dev/null
+++ b/src/views/article/Header.tsx
@@ -0,0 +1,66 @@
+import React from 'react';
+import {
+ chevroneLeft,
+ chevroneRight,
+ arrowLeft,
+} from '../../assets/icons/header';
+import { Logo } from '../../assets/logos';
+import { useNavigate } from 'react-router-dom';
+
+interface HeaderProps {
+ articleId: number;
+ back?: string;
+}
+
+const Header: React.FC = ({ articleId, back }) => {
+ const navigate = useNavigate();
+ return (
+
+ );
+};
+
+export default Header;
diff --git a/src/views/articleeditor/Editor.tsx b/src/views/articleeditor/Editor.tsx
new file mode 100644
index 0000000..38e4e43
--- /dev/null
+++ b/src/views/articleeditor/Editor.tsx
@@ -0,0 +1,297 @@
+import { FC, useEffect, useState } from 'react';
+import axios from '../../axios';
+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-редактор
+
+Добро пожаловать в **Markdown-редактор**!
+Здесь ты можешь писать в формате Markdown и видеть результат **в реальном времени** 👇
+
+---
+
+## 🧱 1. Форматирование текста
+
+Вот примеры базового форматирования:
+
+- **Жирный текст**
+- *Курсивный текст*
+- ***Жирный курсив***
+- ~~Зачёркнутый~~
+
+> 💬 _Цитаты_ можно использовать для выделения текста, заметок или описаний.
+
+---
+
+## 🧩 2. Списки
+
+### 🔹 Маркированный список
+
+- Один
+- Два
+ - Вложенный уровень
+ - Ещё глубже
+- Три
+
+### 🔸 Нумерованный список
+
+1. Первый
+2. Второй
+3. Третий
+ 1. Вложенный
+ 2. Ещё один
+
+---
+
+## ✅ 3. Чеклисты (GFM)
+
+- [x] Поддержка Markdown
+- [x] Подсветка кода
+- [x] Таблицы
+- [x] Эмодзи 😎
+- [ ] Экспорт в PDF (в будущем)
+
+---
+
+## 💻 4. Код и подсветка
+
+Пример **TypeScript**:
+
+\`\`\`tsx
+type User = {
+ name: string;
+ role: "Разработчик" | "Помощник";
+};
+
+function greet(user: User) {
+ return \`Привет, \${user.name}! 👋 Роль: \${user.role}\`;
+}
+
+console.log(greet({ name: "Ты", role: "Разработчик" }));
+\`\`\`
+
+Пример **JavaScript**:
+
+\`\`\`js
+const sum = (a, b) => a + b;
+console.log(sum(2, 3)); // 5
+\`\`\`
+
+Пример **Python**:
+
+\`\`\`python
+def greet(name):
+ return f"Привет, {name}! 👋"
+
+print(greet("Мир"))
+\`\`\`
+
+---
+
+## 📊 5. Таблицы (GFM)
+
+| Имя | Роль | Активен | Эмодзи |
+|-------------|----------------|----------|--------|
+| ChatGPT | Помощник 🤖 | ✅ | 🤓 |
+| Ты | Разработчик 💻 | ✅ | 🚀 |
+| TailwindCSS | Стилизация 🎨 | 🟢 | 💅 |
+
+> Таблицы поддерживают **жирный текст**, _курсив_ и даже \`инлайн-код\` внутри ячеек.
+
+---
+
+## 🔗 6. Ссылки
+
+- [Документация Markdown](https://www.markdownguide.org/)
+- [React Markdown на GitHub](https://github.com/remarkjs/react-markdown)
+- Автоматическая ссылка: https://github.com
+
+---
+
+## 🖼️ 7. Изображения
+
+### Markdown-логотип:
+
+
+
+или
+
+
+
+или если нужно выравнивание по центру
+
+
+

+
+
+
+---
+
+## 🧠 8. Цитаты и вложенность
+
+> 💭 Это обычная цитата.
+>
+> > А это — **вложенная цитата**.
+> >
+> > > Можно вкладывать сколько угодно уровней!
+
+---
+
+## ⚙️ 9. Горизонтальные линии
+
+---
+
+***
+
+---
+
+## 🧮 10. Таблица внутри цитаты
+
+> Вот таблица прямо внутри блока цитаты:
+>
+> | Язык | Назначение |
+> |-------|-------------|
+> | JS | Web-разработка |
+> | TS | Строгая типизация |
+> | PY | Скрипты и AI |
+
+---
+
+## 🧩 11. Встроенный HTML
+
+
+ 📂 Раскрывающийся блок
+ Этот текст виден только после раскрытия!
+
+ - HTML списки работают
+ - И даже жирный текст
+
+
+
+---
+## 🎨 12. Вложенные списки с кодом
+
+- Этапы:
+ 1. Создай проект
+ 2. Добавь зависимости:
+ \`\`\`bash
+ npm install react-markdown remark-gfm rehype-highlight highlight.js
+ \`\`\`
+ 3. Импортируй стили:
+ \`\`\`tsx
+ import "highlight.js/styles/github-dark.css";
+ \`\`\`
+ 4. Готово!
+
+---
+
+## 🚀 13. Финал
+
+Поздравляю! 🎉
+Ты только что увидел все ключевые возможности **Markdown + GFM** в действии.
+
+> ✨ Используй этот текст как шаблон для тестирования рендерера.
+> 💡 Совет: попробуй поменять тему \`highlight.js\` (например \`monokai.css\` или \`atom-one-dark.css\`).
+
+---
+
+**🖤 Конец демонстрации. Спасибо, что используешь Markdown-редактор!**
+
+`,
+ );
+
+ useEffect(() => {
+ onChange(markdown);
+ }, [markdown]);
+
+ // Обработчик вставки
+ const handlePaste = async (
+ e: React.ClipboardEvent,
+ ) => {
+ const items = e.clipboardData.items;
+
+ for (const item of items) {
+ if (item.type.startsWith('image/')) {
+ e.preventDefault(); // предотвращаем вставку картинки как текста
+
+ const file = item.getAsFile();
+ if (!file) return;
+
+ const formData = new FormData();
+ formData.append('file', file);
+
+ try {
+ const response = await axios.post(
+ '/media/upload',
+ formData,
+ {
+ headers: { 'Content-Type': 'multipart/form-data' },
+ },
+ );
+
+ const imageUrl = response.data.url;
+ // Вставляем ссылку на картинку в текст
+ const cursorPos = (e.target as HTMLTextAreaElement)
+ .selectionStart;
+ const newText =
+ markdown.slice(0, cursorPos) +
+ `
` +
+ markdown.slice(cursorPos);
+
+ setMarkdown(newText);
+ } catch (err) {
+ console.error('Ошибка загрузки изображения:', err);
+ }
+ }
+ }
+ };
+
+ return (
+
+ {/* Предпросмотр */}
+
+
+
+ 👀 Предпросмотр
+
+
+
+
+
+ {/* Редактор */}
+
+
+ );
+};
+
+export default MarkdownEditor;
diff --git a/src/views/articleeditor/Header.tsx b/src/views/articleeditor/Header.tsx
new file mode 100644
index 0000000..7109c5d
--- /dev/null
+++ b/src/views/articleeditor/Header.tsx
@@ -0,0 +1,41 @@
+import React from 'react';
+import { arrowLeft } from '../../assets/icons/header';
+import { Logo } from '../../assets/logos';
+import { useNavigate } from 'react-router-dom';
+
+interface HeaderProps {
+ backClick?: () => void;
+}
+
+const Header: React.FC = ({ backClick }) => {
+ const navigate = useNavigate();
+ return (
+
+ );
+};
+
+export default Header;
diff --git a/src/views/articleeditor/MarckDownPreview.tsx b/src/views/articleeditor/MarckDownPreview.tsx
new file mode 100644
index 0000000..36f5e38
--- /dev/null
+++ b/src/views/articleeditor/MarckDownPreview.tsx
@@ -0,0 +1,55 @@
+import { FC } from 'react';
+import ReactMarkdown from 'react-markdown';
+import remarkGfm from 'remark-gfm';
+import rehypeHighlight from 'rehype-highlight';
+import rehypeRaw from 'rehype-raw';
+import rehypeSanitize from 'rehype-sanitize';
+import 'highlight.js/styles/github-dark.css';
+
+import { defaultSchema } from 'hast-util-sanitize';
+import { cn } from '../../lib/cn';
+
+const schema = {
+ ...defaultSchema,
+ attributes: {
+ ...defaultSchema.attributes,
+ div: [
+ ...(defaultSchema.attributes?.div || []),
+ ['style'], // разрешаем атрибут style на div
+ ],
+ },
+};
+
+interface MarkdownPreviewProps {
+ content: string;
+ className?: string;
+}
+
+const MarkdownPreview: FC = ({
+ content,
+ className = '',
+}) => {
+ return (
+
+ );
+};
+
+export default MarkdownPreview;
diff --git a/src/views/home/account/Account.tsx b/src/views/home/account/Account.tsx
new file mode 100644
index 0000000..7dbf7af
--- /dev/null
+++ b/src/views/home/account/Account.tsx
@@ -0,0 +1,56 @@
+import { Navigate, Route, Routes } from 'react-router-dom';
+import AccountMenu from './AccoutMenu';
+import RightPanel from './RightPanel';
+import MissionsBlock from './MissionsBlock';
+import ContestsBlock from './ContestsBlock';
+import ArticlesBlock from './ArticlesBlock';
+import { useAppDispatch } from '../../../redux/hooks';
+import { useEffect } from 'react';
+import { setMenuActivePage } from '../../../redux/slices/store';
+
+const Account = () => {
+ const dispatch = useAppDispatch();
+
+ useEffect(() => {
+ dispatch(setMenuActivePage('account'));
+ }, []);
+
+ return (
+
+
+
+
+
+
+ }
+ />
+ }
+ />
+ }
+ />
+
+ }
+ />
+
+
+
+
+
+
+
+
+ );
+};
+
+export default Account;
diff --git a/src/views/home/account/AccoutMenu.tsx b/src/views/home/account/AccoutMenu.tsx
new file mode 100644
index 0000000..f8a51b1
--- /dev/null
+++ b/src/views/home/account/AccoutMenu.tsx
@@ -0,0 +1,94 @@
+import { Openbook, Cup, Clipboard } from '../../../assets/icons/menu';
+
+import React 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}
+
+ );
+};
+
+const AccountMenu = () => {
+ const menuItems = [
+ {
+ text: 'Задачи',
+ href: '/home/account/missions',
+ icon: Clipboard,
+ page: 'account',
+ profilePage: 'missions',
+ },
+ {
+ text: 'Статьи',
+ href: '/home/account/articles',
+ icon: Openbook,
+ page: 'account',
+ profilePage: 'articles',
+ },
+ {
+ text: 'Контесты',
+ href: '/home/account/contests',
+ icon: Cup,
+ page: 'account',
+ profilePage: 'contests',
+ },
+ ];
+
+ const activeProfilePage = useAppSelector(
+ (state) => state.store.menu.activeProfilePage,
+ );
+
+ console.log('active', [activeProfilePage]);
+
+ return (
+
+ {menuItems.map((v, i) => (
+
+ ))}
+
+ );
+};
+
+export default AccountMenu;
diff --git a/src/views/home/account/ArticlesBlock.tsx b/src/views/home/account/ArticlesBlock.tsx
new file mode 100644
index 0000000..1d4f141
--- /dev/null
+++ b/src/views/home/account/ArticlesBlock.tsx
@@ -0,0 +1,124 @@
+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 { useNavigate } from 'react-router-dom';
+
+export interface ArticleItemProps {
+ id: number;
+ name: string;
+ tags: string[];
+}
+
+const ArticleItem: React.FC = ({ id, name, tags }) => {
+ const navigate = useNavigate();
+ return (
+ {
+ navigate(`/article/${id}?back=/home/account/articles`);
+ }}
+ >
+
+
+ #{id}
+
+
+ {name}
+
+
+
+ {tags.map((v, i) => (
+
+ {v}
+
+ ))}
+
+
+

{
+ e.stopPropagation();
+ navigate(
+ `/article/create?back=/home/account/articles&articleId=${id}`,
+ );
+ }}
+ />
+
+ );
+};
+
+interface ArticlesBlockProps {
+ className?: string;
+}
+
+const ArticlesBlock: FC = ({ className = '' }) => {
+ const dispatch = useAppDispatch();
+ const articles = useAppSelector((state) => state.articles.articles);
+ const [active, setActive] = useState(true);
+
+ useEffect(() => {
+ dispatch(setMenuActiveProfilePage('articles'));
+ dispatch(fetchArticles({}));
+ }, []);
+ return (
+
+
+
{
+ setActive(!active);
+ }}
+ >
+
Мои статьи
+

+
+
+
+
+ {articles.map((v, i) => (
+
+ ))}
+
+
+
+
+
+ );
+};
+
+export default ArticlesBlock;
diff --git a/src/views/home/account/ContestsBlock.tsx b/src/views/home/account/ContestsBlock.tsx
new file mode 100644
index 0000000..91b13ac
--- /dev/null
+++ b/src/views/home/account/ContestsBlock.tsx
@@ -0,0 +1,18 @@
+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
new file mode 100644
index 0000000..1fce2a8
--- /dev/null
+++ b/src/views/home/account/MissionsBlock.tsx
@@ -0,0 +1,19 @@
+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/RightPanel.tsx b/src/views/home/account/RightPanel.tsx
new file mode 100644
index 0000000..b1b63d8
--- /dev/null
+++ b/src/views/home/account/RightPanel.tsx
@@ -0,0 +1,118 @@
+import { PrimaryButton } from '../../../components/button/PrimaryButton';
+import { ReverseButton } from '../../../components/button/ReverseButton';
+import { useAppDispatch, useAppSelector } from '../../../redux/hooks';
+import { logout } from '../../../redux/slices/auth';
+import { OpenBook, Clipboard, Cup } from '../../../assets/icons/account';
+import { FC } from 'react';
+
+interface StatisticItemProps {
+ icon: string;
+ title: string;
+ count?: number;
+ countLastWeek?: number;
+}
+const StatisticItem: FC = ({
+ title,
+ icon,
+ count = 0,
+ countLastWeek = 0,
+}) => {
+ return (
+
+

+
+
+
+ {'За 7 дней '}
+ {countLastWeek}
+
+
+
+ );
+};
+
+const RightPanel = () => {
+ const dispatch = useAppDispatch();
+ const name = useAppSelector((state) => state.auth.username);
+ const email = useAppSelector((state) => state.auth.email);
+ return (
+
+
+
+
+
+ {name}
+
+
+ {email}
+
+
+ Топ 50%
+
+
+
+
+
{}}
+ text="Редактировать"
+ className="w-full"
+ />
+
+
+
+
+ {'Статистика решений'}
+
+
+
+
+
+
+ {'Статистика созданий'}
+
+
+
+
+
+
+ {
+ dispatch(logout());
+ }}
+ text="Выход"
+ color="error"
+ />
+
+ );
+};
+
+export default RightPanel;
diff --git a/src/views/home/articles/ArticleItem.tsx b/src/views/home/articles/ArticleItem.tsx
index 9676fab..6ab32dd 100644
--- a/src/views/home/articles/ArticleItem.tsx
+++ b/src/views/home/articles/ArticleItem.tsx
@@ -1,4 +1,5 @@
-import { cn } from "../../../lib/cn";
+import { useNavigate } from 'react-router-dom';
+import { cn } from '../../../lib/cn';
export interface ArticleItemProps {
id: number;
@@ -6,17 +7,21 @@ export interface ArticleItemProps {
tags: string[];
}
-const ArticleItem: React.FC = ({
- id, name, tags
-}) => {
+const ArticleItem: React.FC = ({ id, name, tags }) => {
+ const navigate = useNavigate();
return (
-
+
{
+ navigate(`/article/${id}`);
+ }}
+ >
-
#{id}
@@ -25,15 +30,18 @@ const ArticleItem: React.FC
= ({
- {tags.map((v, i) =>
-
+ {tags.map((v, i) => (
+
{v}
- )}
+ ))}
-
);
};
diff --git a/src/views/home/articles/Articles.tsx b/src/views/home/articles/Articles.tsx
index de5f1d4..13519fd 100644
--- a/src/views/home/articles/Articles.tsx
+++ b/src/views/home/articles/Articles.tsx
@@ -1,9 +1,10 @@
-import { useEffect } from "react";
-import { SecondaryButton } from "../../../components/button/SecondaryButton";
-import { useAppDispatch } from "../../../redux/hooks";
-import ArticleItem from "./ArticleItem";
-import { setMenuActivePage } from "../../../redux/slices/store";
-
+import { useEffect } from 'react';
+import { SecondaryButton } from '../../../components/button/SecondaryButton';
+import { useAppDispatch, useAppSelector } from '../../../redux/hooks';
+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;
@@ -11,158 +12,45 @@ export interface Article {
tags: string[];
}
-
const Articles = () => {
-
const dispatch = useAppDispatch();
+ const navigate = useNavigate();
- const articles: Article[] = [
- {
- "id": 1,
- "name": "Todo List App",
- "tags": ["Sertificated", "state", "list"],
- },
- {
- "id": 2,
- "name": "Search Filter Component",
- "tags": ["filter", "props", "hooks"],
- },
- {
- "id": 3,
- "name": "User Card List",
- "tags": ["components", "props", "array"],
- },
- {
- "id": 4,
- "name": "Theme Switcher",
- "tags": ["Sertificated", "theme", "hooks"],
- },
- {
- "id": 2,
- "name": "Search Filter Component",
- "tags": ["filter", "props", "hooks"],
- },
- {
- "id": 3,
- "name": "User Card List",
- "tags": ["components", "props", "array"],
- },
- {
- "id": 4,
- "name": "Theme Switcher",
- "tags": ["Sertificated", "theme", "hooks"],
- },
- {
- "id": 2,
- "name": "Search Filter Component",
- "tags": ["filter", "props", "hooks"],
- },
- {
- "id": 3,
- "name": "User Card List",
- "tags": ["components", "props", "array"],
- },
- {
- "id": 4,
- "name": "Theme Switcher",
- "tags": ["Sertificated", "theme", "hooks"],
- },
- {
- "id": 2,
- "name": "Search Filter Component",
- "tags": ["filter", "props", "hooks"],
- },
- {
- "id": 3,
- "name": "User Card List",
- "tags": ["components", "props", "array"],
- },
- {
- "id": 4,
- "name": "Theme Switcher",
- "tags": ["Sertificated", "theme", "hooks"],
- },
- {
- "id": 2,
- "name": "Search Filter Component",
- "tags": ["filter", "props", "hooks"],
- },
- {
- "id": 3,
- "name": "User Card List",
- "tags": ["components", "props", "array"],
- },
- {
- "id": 4,
- "name": "Theme Switcher",
- "tags": ["Sertificated", "theme", "hooks"],
- },
- {
- "id": 2,
- "name": "Search Filter Component",
- "tags": ["filter", "props", "hooks"],
- },
- {
- "id": 3,
- "name": "User Card List",
- "tags": ["components", "props", "array"],
- },
- {
- "id": 4,
- "name": "Theme Switcher",
- "tags": ["Sertificated", "theme", "hooks"],
- },
- {
- "id": 2,
- "name": "Search Filter Component",
- "tags": ["filter", "props", "hooks"],
- },
- {
- "id": 3,
- "name": "User Card List",
- "tags": ["components", "props", "array"],
- },
- {
- "id": 4,
- "name": "Theme Switcher",
- "tags": ["Sertificated", "theme", "hooks"],
- }
- ];
+ const articles = useAppSelector((state) => state.articles.articles);
+ const status = useAppSelector((state) => state.articles.statuses.fetchAll);
- useEffect(() => {
- dispatch(setMenuActivePage("articles"))
- }, []);
+ useEffect(() => {
+ dispatch(setMenuActivePage('articles'));
+ dispatch(fetchArticles({}));
+ }, []);
+
+ if (status == 'loading') return
Загрузка...
;
return (
-
Статьи
{ }}
+ onClick={() => {
+ navigate('/article/create');
+ }}
text="Создать статью"
className="absolute right-0"
/>
-
-
-
+
-
{articles.map((v, i) => (
))}
-
-
- pages
-
+
pages
);
diff --git a/src/views/home/auth/Login.tsx b/src/views/home/auth/Login.tsx
index e1d5432..548be70 100644
--- a/src/views/home/auth/Login.tsx
+++ b/src/views/home/auth/Login.tsx
@@ -1,112 +1,133 @@
-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 { loginUser } from "../../../redux/slices/auth";
+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 { loginUser } from '../../../redux/slices/auth';
// import { cn } from "../../../lib/cn";
-import { setMenuActivePage } from "../../../redux/slices/store";
-import { Balloon } from "../../../assets/icons/auth";
-import { SecondaryButton } from "../../../components/button/SecondaryButton";
-import { googleLogo } from "../../../assets/icons/input";
+import { setMenuActivePage } from '../../../redux/slices/store';
+import { Balloon } from '../../../assets/icons/auth';
+import { SecondaryButton } from '../../../components/button/SecondaryButton';
+import { googleLogo } from '../../../assets/icons/input';
const Login = () => {
- const dispatch = useAppDispatch();
- const navigate = useNavigate();
+ const dispatch = useAppDispatch();
+ const navigate = useNavigate();
- const [username, setUsername] = useState
("");
- const [password, setPassword] = useState("");
- const [submitClicked, setSubmitClicked] = useState(false);
+ const [username, setUsername] = useState('');
+ const [password, setPassword] = useState('');
+ const [submitClicked, setSubmitClicked] = useState(false);
- const { status, jwt } = useAppSelector((state) => state.auth);
+ const { status, jwt } = useAppSelector((state) => state.auth);
+ // const [err, setErr] = useState("");
- // const [err, setErr] = useState("");
+ // После успешного логина
+ useEffect(() => {
+ dispatch(setMenuActivePage('account'));
+ console.log(submitClicked);
+ }, []);
- // После успешного логина
- useEffect(() => {
- dispatch(setMenuActivePage("account"))
- }, []);
+ useEffect(() => {
+ if (jwt) {
+ navigate('/home/account'); // или другая страница после входа
+ }
+ }, [jwt]);
- useEffect(() => {
- if (jwt) {
- navigate("/home/offices"); // или другая страница после входа
- }
- }, [jwt]);
+ const handleLogin = () => {
+ // setErr(err == "" ? "Неверная почта и/или пароль" : "");
+ setSubmitClicked(true);
- const handleLogin = () => {
- // setErr(err == "" ? "Неверная почта и/или пароль" : "");
- setSubmitClicked(true);
+ if (!username || !password) return;
- if (!username || !password) return;
+ dispatch(loginUser({ username, password }));
+ };
- dispatch(loginUser({ username, password }));
- };
+ return (
+
+
+
+

+
+
+
+
+ С возвращением
+
+
+ Вход в аккаунт
+
+
- return (
-
-
-
-

-
-
-
-
- С возвращением
+
{
+ setUsername(v);
+ }}
+ placeholder="login"
+ />
+
{
+ setPassword(v);
+ }}
+ placeholder="abCD1234"
+ />
+
+
+
+ Забыли пароль?
+
+
+
+
+
+
{}}>
+
+

+ Вход с Google
+
+
+
+
+
+
+ Нет аккаунта?{' '}
+
+ Регистрация
+
+
+
+
-
- Вход в аккаунт
-
-
-
-
-
{ setUsername(v) }} placeholder="login" />
-
{ setPassword(v) }} placeholder="abCD1234" />
-
-
-
- Забыли пароль?
-
-
-
-
-
-
-
{ }}
- >
-
-

- Вход с Google
-
-
-
-
-
-
-
-
- Нет аккаунта?
- Регистрация
-
-
-
-
-
-
-
- );
+ );
};
export default Login;
diff --git a/src/views/home/auth/Register.tsx b/src/views/home/auth/Register.tsx
index 4f39ef7..d341b39 100644
--- a/src/views/home/auth/Register.tsx
+++ b/src/views/home/auth/Register.tsx
@@ -1,125 +1,169 @@
-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 { registerUser } from "../../../redux/slices/auth";
+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 { registerUser } from '../../../redux/slices/auth';
// import { cn } from "../../../lib/cn";
-import { setMenuActivePage } from "../../../redux/slices/store";
-import { Balloon } from "../../../assets/icons/auth";
-import { Link } from "react-router-dom";
-import { SecondaryButton } from "../../../components/button/SecondaryButton";
-import { Checkbox } from "../../../components/checkbox/Checkbox";
-import { googleLogo } from "../../../assets/icons/input";
-
+import { setMenuActivePage } from '../../../redux/slices/store';
+import { Balloon } from '../../../assets/icons/auth';
+import { Link } from 'react-router-dom';
+import { SecondaryButton } from '../../../components/button/SecondaryButton';
+import { Checkbox } from '../../../components/checkbox/Checkbox';
+import { googleLogo } from '../../../assets/icons/input';
const Register = () => {
- const dispatch = useAppDispatch();
- const navigate = useNavigate();
+ const dispatch = useAppDispatch();
+ const navigate = useNavigate();
- const [username, setUsername] = useState
("");
- const [email, setEmail] = useState("");
- const [password, setPassword] = useState("");
- const [confirmPassword, setConfirmPassword] = useState("");
- const [submitClicked, setSubmitClicked] = useState(false);
+ const [username, setUsername] = useState('');
+ const [email, setEmail] = useState('');
+ const [password, setPassword] = useState('');
+ const [confirmPassword, setConfirmPassword] = useState('');
+ const [submitClicked, setSubmitClicked] = useState(false);
- const { status, jwt } = useAppSelector((state) => state.auth);
+ const { status, jwt } = useAppSelector((state) => state.auth);
- // После успешной регистрации — переход в систему
+ // После успешной регистрации — переход в систему
- useEffect(() => {
- dispatch(setMenuActivePage("account"))
- }, []);
+ useEffect(() => {
+ dispatch(setMenuActivePage('account'));
+ }, []);
- useEffect(() => {
- if (jwt) {
- navigate("/home");
- }
- }, [jwt]);
+ useEffect(() => {
+ if (jwt) {
+ navigate('/home/account');
+ }
+ console.log(submitClicked);
+ }, [jwt]);
- const handleRegister = () => {
- setSubmitClicked(true);
+ const handleRegister = () => {
+ setSubmitClicked(true);
- if (!username || !email || !password || !confirmPassword) return;
- if (password !== confirmPassword) return;
+ if (!username || !email || !password || !confirmPassword) return;
+ if (password !== confirmPassword) return;
- dispatch(registerUser({ username, email, password }));
- };
+ dispatch(registerUser({ username, email, password }));
+ };
- return (
-
-
-
-

-
-
-
-
- Добро пожаловать
+ return (
+
+
+
+

+
+
+
+
+ Добро пожаловать
+
+
+ Регистрация
+
+
+
+
{
+ setEmail(v);
+ }}
+ placeholder="example@gmail.com"
+ />
+
{
+ setUsername(v);
+ }}
+ placeholder="login"
+ />
+
{
+ setPassword(v);
+ }}
+ placeholder="abCD1234"
+ />
+
{
+ setConfirmPassword(v);
+ }}
+ placeholder="abCD1234"
+ />
+
+
+ {
+ value;
+ }}
+ className="p-0 w-fit m-[2.75px]"
+ size="md"
+ color="secondary"
+ variant="default"
+ />
+
+ Я принимаю{' '}
+
+ политику конфиденциальности
+
+
+
+
+
+
handleRegister()}
+ text={
+ status === 'loading'
+ ? 'Регистрация...'
+ : 'Регистрация'
+ }
+ disabled={status === 'loading'}
+ />
+ {}}>
+
+

+ Регистрация с Google
+
+
+
+
+
+
+ Уже есть аккаунт?{' '}
+
+ Авторизация
+
+
+
+
-
- Регистрация
-
-
-
-
-
{setEmail(v)}} placeholder="example@gmail.com" />
-
{setUsername(v)}} placeholder="login" />
-
{setPassword(v)}} placeholder="abCD1234" />
-
{setConfirmPassword(v)}} placeholder="abCD1234" />
-
-
- { value; }}
- className="p-0 w-fit m-[2.75px]"
- size="md"
- color="secondary"
- variant="default" />
-
- Я принимаю
- политику конфиденциальности
-
-
-
-
-
-
-
handleRegister()}
- text={status === "loading" ? "Регистрация..." : "Регистрация"}
- disabled={status === "loading"}
- />
- { }}
- >
-
-

- Регистрация с Google
-
-
-
-
-
-
-
- Уже есть аккаунт?
- Авторизация
-
-
-
-
-
-
-
- );
+ );
};
export default Register;
diff --git a/src/views/home/contest/Contest.tsx b/src/views/home/contest/Contest.tsx
new file mode 100644
index 0000000..d184dd4
--- /dev/null
+++ b/src/views/home/contest/Contest.tsx
@@ -0,0 +1,44 @@
+import { useEffect } from 'react';
+import { useAppDispatch, useAppSelector } from '../../../redux/hooks';
+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';
+
+export interface Article {
+ id: number;
+ name: string;
+ tags: string[];
+}
+
+const Contest = () => {
+ const { contestId } = useParams<{ contestId: string }>();
+ const contestIdNumber =
+ contestId && /^\d+$/.test(contestId) ? parseInt(contestId, 10) : null;
+ if (contestIdNumber === null) {
+ return
;
+ }
+ const dispatch = useAppDispatch();
+ const contest = useAppSelector((state) => state.contests.selectedContest);
+
+ useEffect(() => {
+ dispatch(setMenuActivePage('contest'));
+ }, []);
+
+ useEffect(() => {
+ dispatch(fetchContestById(contestIdNumber));
+ }, [contestIdNumber]);
+
+ return (
+
+
+ }
+ />
+
+
+ );
+};
+
+export default Contest;
diff --git a/src/views/home/contest/MissionItem.tsx b/src/views/home/contest/MissionItem.tsx
new file mode 100644
index 0000000..ee9579b
--- /dev/null
+++ b/src/views/home/contest/MissionItem.tsx
@@ -0,0 +1,68 @@
+import { cn } from '../../../lib/cn';
+import { IconError, IconSuccess } from '../../../assets/icons/missions';
+import { useNavigate } from 'react-router-dom';
+import { useLocation } from 'react-router-dom';
+
+export interface MissionItemProps {
+ id: number;
+ name: string;
+ timeLimit?: number;
+ memoryLimit?: number;
+ 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,
+ timeLimit = 1000,
+ memoryLimit = 256 * 1024 * 1024,
+ type,
+ status,
+}) => {
+ const navigate = useNavigate();
+ const location = useLocation();
+ const path = location.pathname + location.search;
+
+ return (
+ {
+ navigate(`/mission/${id}?back=${path}`);
+ }}
+ >
+
#{id}
+
{name}
+
+ стандартный ввод/вывод {formatMilliseconds(timeLimit)},{' '}
+ {formatBytesToMB(memoryLimit)}
+
+
+ {status == 'error' &&

}
+ {status == 'success' &&

}
+
+
+ );
+};
+
+export default MissionItem;
diff --git a/src/views/home/contest/Missions.tsx b/src/views/home/contest/Missions.tsx
new file mode 100644
index 0000000..535cf05
--- /dev/null
+++ b/src/views/home/contest/Missions.tsx
@@ -0,0 +1,43 @@
+import { FC } from 'react';
+import MissionItem from './MissionItem';
+import { Contest } from '../../../redux/slices/contests';
+
+export interface Article {
+ id: number;
+ name: string;
+ tags: string[];
+}
+
+interface ContestMissionsProps {
+ contest: Contest | null;
+}
+
+const ContestMissions: FC = ({ contest }) => {
+ if (!contest) {
+ return <>>;
+ }
+
+ return (
+
+
+
+
+ {contest?.name} {contest.id}
+
+
+ {contest.missions.map((v, i) => (
+
+ ))}
+
+
+
+ );
+};
+
+export default ContestMissions;
diff --git a/src/views/home/contest/Submissions.tsx b/src/views/home/contest/Submissions.tsx
new file mode 100644
index 0000000..e69de29
diff --git a/src/views/home/contests/ContestItem.tsx b/src/views/home/contests/ContestItem.tsx
index 5451ae7..c2fc6fd 100644
--- a/src/views/home/contests/ContestItem.tsx
+++ b/src/views/home/contests/ContestItem.tsx
@@ -1,70 +1,122 @@
-import { cn } from "../../../lib/cn";
+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;
- authors: string[];
startAt: string;
- registerAt: string;
duration: number;
members: number;
- statusRegister: "reg" | "nonreg";
- type: "first" | "second";
+ statusRegister: 'reg' | 'nonreg';
+ type: 'first' | 'second';
}
function formatDate(dateString: string): string {
- const date = new Date(dateString);
+ 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 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");
+ const hours = date.getHours().toString().padStart(2, '0');
+ const minutes = date.getMinutes().toString().padStart(2, '0');
- return `${day}/${month}/${year}\n${hours}:${minutes}`;
+ 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, authors, startAt, registerAt, duration, members, statusRegister, type
+ id,
+ name,
+ startAt,
+ duration,
+ members,
+ statusRegister,
+ type,
}) => {
+ const navigate = useNavigate();
+
const now = new Date();
const waitTime = new Date(startAt).getTime() - now.getTime();
return (
-
-
- {name}
+
{
+ navigate(`/contest/${id}`);
+ }}
+ >
+
{name}
+
+ {/* {authors.map((v, i) =>
{v}
)} */}
+ valavshonok
-
- {authors.map((v, i) =>
{v}
)}
-
-
+
{formatDate(startAt)}
-
- {duration}
-
- {
- waitTime > 0 &&
-
- {waitTime}
+
{formatWaitTime(duration)}
+ {waitTime > 0 && (
+
+ {'До начала\n' + formatWaitTime(waitTime)}
- }
-
- {members}
+ )}
+
+
{members}
+
-
- {statusRegister}
+
+ {statusRegister == 'reg' ? (
+ <>
+ {' '}
+
{
+ e.stopPropagation();
+ }}
+ text="Регистрация"
+ />
+ >
+ ) : (
+ <>
+ {' '}
+ {
+ e.stopPropagation();
+ }}
+ text="Вы записаны"
+ />
+ >
+ )}
-
);
};
diff --git a/src/views/home/contests/Contests.tsx b/src/views/home/contests/Contests.tsx
index 2d68158..d3ad1eb 100644
--- a/src/views/home/contests/Contests.tsx
+++ b/src/views/home/contests/Contests.tsx
@@ -1,129 +1,84 @@
-import { useEffect } from "react";
-import { SecondaryButton } from "../../../components/button/SecondaryButton";
-import { cn } from "../../../lib/cn";
-import { useAppDispatch } from "../../../redux/hooks";
-import ContestsBlock from "./ContestsBlock";
-import { setMenuActivePage } from "../../../redux/slices/store";
-
-
-interface Contest {
- id: number;
- name: string;
- authors: string[];
- startAt: string;
- registerAt: string;
- duration: number;
- members: number;
- statusRegister: "reg" | "nonreg";
-}
-
-
+import { useEffect, useState } from 'react';
+import { SecondaryButton } from '../../../components/button/SecondaryButton';
+import { cn } from '../../../lib/cn';
+import { useAppDispatch, useAppSelector } from '../../../redux/hooks';
+import ContestsBlock from './ContestsBlock';
+import { setMenuActivePage } from '../../../redux/slices/store';
+import { fetchContests } from '../../../redux/slices/contests';
+import ModalCreateContest from './ModalCreate';
const Contests = () => {
-
const dispatch = useAppDispatch();
const now = new Date();
- const contests: Contest[] = [
- // === Прошедшие контесты ===
- {
- id: 1,
- name: "Code Marathon 2025",
- authors: ["tourist", "Petr", "Semen", "Rotar"],
- startAt: "2025-09-15T10:00:00.000Z",
- registerAt: "2025-09-10T10:00:00.000Z",
- duration: 180,
- members: 4821,
- statusRegister: "reg",
- },
- {
- id: 2,
- name: "Autumn Cup 2025",
- authors: ["awoo", "Benq"],
- startAt: "2025-09-25T17:00:00.000Z",
- registerAt: "2025-09-20T17:00:00.000Z",
- duration: 150,
- members: 3670,
- statusRegister: "nonreg",
- },
- // === Контесты, которые сейчас идут ===
- {
- id: 3,
- name: "Halloween Challenge",
- authors: ["Errichto", "Radewoosh"],
- startAt: "2025-10-29T10:00:00.000Z", // начался сегодня
- registerAt: "2025-10-25T10:00:00.000Z",
- duration: 240,
- members: 5123,
- statusRegister: "reg",
- },
- {
- id: 4,
- name: "October Blitz",
- authors: ["neal", "Um_nik"],
- startAt: "2025-10-29T12:00:00.000Z",
- registerAt: "2025-10-24T12:00:00.000Z",
- duration: 300,
- members: 2890,
- statusRegister: "nonreg",
- },
+ const [modalActive, setModalActive] = useState
(false);
- // === Контесты, которые еще не начались ===
- {
- id: 5,
- name: "Winter Warmup",
- authors: ["tourist", "rng_58"],
- startAt: "2025-11-05T18:00:00.000Z",
- registerAt: "2025-11-01T18:00:00.000Z",
- duration: 180,
- members: 2100,
- statusRegister: "reg",
- },
- {
- id: 6,
- name: "Global Coding Cup",
- authors: ["maroonrk", "kostka"],
- startAt: "2025-11-12T15:00:00.000Z",
- registerAt: "2025-11-08T15:00:00.000Z",
- duration: 240,
- members: 1520,
- statusRegister: "nonreg",
- },
- ];
+ // Берём данные из Redux
+ const contests = useAppSelector((state) => state.contests.contests);
+ const status = useAppSelector((state) => state.contests.statuses.create);
+ const error = useAppSelector((state) => state.contests.error);
+ // При загрузке страницы — выставляем активную вкладку и подгружаем контесты
useEffect(() => {
- dispatch(setMenuActivePage("contests"))
+ dispatch(setMenuActivePage('contests'));
+ dispatch(fetchContests({}));
}, []);
- return (
-
-
+ if (status == 'loading') {
+ return (
+
Загрузка контестов...
+ );
+ }
+ if (error) {
+ return
Ошибка: {error}
;
+ }
+
+ return (
+
+
-
+
Контесты
{ }}
- text="Создать группу"
+ onClick={() => {
+ setModalActive(true);
+ }}
+ text="Создать контест"
className="absolute right-0"
/>
-
+
{
+ const endTime = new Date(contest.endsAt).getTime();
+ return endTime >= now.getTime();
+ })}
+ />
-
- {
- const endTime = new Date(contest.startAt).getTime() + contest.duration * 60 * 1000;
- return endTime >= now.getTime();
- })} />
- {
- const endTime = new Date(contest.startAt).getTime() + contest.duration * 60 * 1000;
- return endTime < now.getTime();
- })} />
+ {
+ const endTime = new Date(contest.endsAt).getTime();
+ return endTime < now.getTime();
+ })}
+ />
+
+
);
};
diff --git a/src/views/home/contests/ContestsBlock.tsx b/src/views/home/contests/ContestsBlock.tsx
index ec14e55..1740745 100644
--- a/src/views/home/contests/ContestsBlock.tsx
+++ b/src/views/home/contests/ContestsBlock.tsx
@@ -1,63 +1,75 @@
-import { useState, FC } from "react";
-import { cn } from "../../../lib/cn";
-import { ChevroneDown } from "../../../assets/icons/groups";
-import ContestItem from "./ContestItem";
+import { useState, FC } from 'react';
+import { cn } from '../../../lib/cn';
+import { ChevroneDown } from '../../../assets/icons/groups';
+import ContestItem from './ContestItem';
+import { Contest } from '../../../redux/slices/contests';
-
-interface Contest {
- id: number;
- name: string;
- authors: string[];
- startAt: string;
- registerAt: string;
- duration: number;
- members: number;
- statusRegister: "reg" | "nonreg";
-}
-
-interface GroupsBlockProps {
+interface ContestsBlockProps {
contests: Contest[];
title: string;
className?: string;
}
-
-const GroupsBlock: FC
= ({ contests, title, className }) => {
-
-
- const [active, setActive] = useState(title != "Скрытые");
-
+const ContestsBlock: FC = ({
+ contests,
+ title,
+ className,
+}) => {
+ const [active, setActive] = useState(title != 'Скрытые');
return (
-
-
-
+
{
- setActive(!active)
- }}>
+ setActive(!active);
+ }}
+ >
{title}
-

+
-
+
- {
- contests.map((v, i) => )
- }
+ {contests.map((v, i) => (
+
+ ))}
-
);
};
-export default GroupsBlock;
+export default ContestsBlock;
diff --git a/src/views/home/contests/ModalCreate.tsx b/src/views/home/contests/ModalCreate.tsx
new file mode 100644
index 0000000..ac9cc1c
--- /dev/null
+++ b/src/views/home/contests/ModalCreate.tsx
@@ -0,0 +1,191 @@
+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 { createContest } from '../../../redux/slices/contests';
+import { CreateContestBody } from '../../../redux/slices/contests';
+import DateRangeInput from '../../../components/input/DateRangeInput';
+
+interface ModalCreateContestProps {
+ active: boolean;
+ setActive: (value: boolean) => void;
+}
+
+const ModalCreateContest: FC
= ({
+ active,
+ setActive,
+}) => {
+ const dispatch = useAppDispatch();
+ const status = useAppSelector((state) => state.contests.statuses.create);
+
+ const [form, setForm] = useState({
+ name: '',
+ description: '',
+ scheduleType: 'AlwaysOpen',
+ visibility: 'Public',
+ startsAt: null,
+ endsAt: null,
+ attemptDurationMinutes: null,
+ maxAttempts: null,
+ allowEarlyFinish: false,
+ groupId: null,
+ missionIds: null,
+ articleIds: null,
+ participantIds: null,
+ organizerIds: null,
+ });
+
+ useEffect(() => {
+ if (status === 'successful') {
+ setActive(false);
+ }
+ }, [status]);
+
+ const handleChange = (key: keyof CreateContestBody, value: any) => {
+ setForm((prev) => ({ ...prev, [key]: value }));
+ };
+
+ const handleSubmit = () => {
+ dispatch(createContest(form));
+ };
+
+ return (
+
+
+
+ Создать контест
+
+
+
handleChange('name', v)}
+ />
+
+
handleChange('description', v)}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Даты начала и конца */}
+
+
+
+
+ {/* Продолжительность и лимиты */}
+
+
+ handleChange('attemptDurationMinutes', Number(v))
+ }
+ />
+ handleChange('maxAttempts', Number(v))}
+ />
+
+
+ {/* Разрешить раннее завершение */}
+
+
+ handleChange('allowEarlyFinish', e.target.checked)
+ }
+ />
+
+
+
+ {/* Кнопки */}
+
+
+
setActive(false)}
+ text="Отмена"
+ />
+
+
+
+ );
+};
+
+export default ModalCreateContest;
diff --git a/src/views/home/groups/Group.tsx b/src/views/home/groups/Group.tsx
new file mode 100644
index 0000000..cc6ad00
--- /dev/null
+++ b/src/views/home/groups/Group.tsx
@@ -0,0 +1,26 @@
+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 d4821d4..d7f579f 100644
--- a/src/views/home/groups/GroupItem.tsx
+++ b/src/views/home/groups/GroupItem.tsx
@@ -1,53 +1,82 @@
-import { cn } from "../../../lib/cn";
-import { Book, UserAdd, Edit, EyeClosed, EyeOpen } from "../../../assets/icons/groups";
+import { cn } from '../../../lib/cn';
+import {
+ Book,
+ UserAdd,
+ Edit,
+ EyeClosed,
+ EyeOpen,
+} from '../../../assets/icons/groups';
+import { useNavigate } from 'react-router-dom';
+import { GroupUpdate } from './Groups';
export interface GroupItemProps {
id: number;
- role: "menager" | "member" | "owner" | "viewer";
+ role: 'menager' | 'member' | 'owner' | 'viewer';
visible: boolean;
name: string;
+ description: string;
+ setUpdateActive: (value: any) => void;
+ setUpdateGroup: (value: GroupUpdate) => void;
}
-
interface IconComponentProps {
src: string;
+ onClick?: () => void;
}
-const IconComponent: React.FC = ({
- src
-}) => {
-
- return
-}
+const IconComponent: React.FC = ({ src, onClick }) => {
+ return (
+
{
+ e.stopPropagation();
+ if (onClick) onClick();
+ }}
+ className="hover:bg-liquid-light rounded-[5px] cursor-pointer transition-all duration-300"
+ />
+ );
+};
const GroupItem: React.FC = ({
- id, name, visible, role
+ id,
+ name,
+ visible,
+ role,
+ description,
+ setUpdateGroup,
+ setUpdateActive,
}) => {
+ const navigate = useNavigate();
+
return (
-
+
navigate(`/group/${id}`)}
+ >
-

+
-
- {name}
-
+
{name}
- {
- (role == "menager" || role == "owner") &&
- }
- {
- (role == "menager" || role == "owner") &&
- }
- {
- visible == false &&
- }
- {
- visible == true &&
- }
+ {(role == 'menager' || role == 'owner') && (
+
+ )}
+ {(role == 'menager' || role == 'owner') && (
+ {
+ setUpdateGroup({ id, name, description });
+ setUpdateActive(true);
+ }}
+ />
+ )}
+ {visible == false && }
+ {visible == true && }
diff --git a/src/views/home/groups/Groups.tsx b/src/views/home/groups/Groups.tsx
index 34559f6..64b3692 100644
--- a/src/views/home/groups/Groups.tsx
+++ b/src/views/home/groups/Groups.tsx
@@ -1,69 +1,124 @@
-import { useEffect } from "react";
-import { SecondaryButton } from "../../../components/button/SecondaryButton";
-import { cn } from "../../../lib/cn";
-import { useAppDispatch } from "../../../redux/hooks";
-import GroupsBlock from "./GroupsBlock";
-import { setMenuActivePage } from "../../../redux/slices/store";
+import { useEffect, useMemo, useState } from 'react';
+import { SecondaryButton } from '../../../components/button/SecondaryButton';
+import { cn } from '../../../lib/cn';
+import { useAppDispatch, useAppSelector } from '../../../redux/hooks';
+import GroupsBlock from './GroupsBlock';
+import { setMenuActivePage } from '../../../redux/slices/store';
+import { fetchMyGroups } from '../../../redux/slices/groups';
+import ModalCreate from './ModalCreate';
+import ModalUpdate from './ModalUpdate';
-
-export interface Group {
+export interface GroupUpdate {
id: number;
- role: "menager" | "member" | "owner" | "viewer";
- visible: boolean;
name: string;
+ description: string;
}
-
const Groups = () => {
+ const [modalActive, setModalActive] = useState
(false);
+ const [modelUpdateActive, setModalUpdateActive] = useState(false);
+ const [updateGroup, setUpdateGroup] = useState({
+ id: 0,
+ name: '',
+ description: '',
+ });
const dispatch = useAppDispatch();
- const groups: Group[] = [
- { id: 1, role: "owner", name: "Main Administration", visible: true },
- { id: 2, role: "menager", name: "Project Managers", visible: true },
- { id: 3, role: "member", name: "Developers", visible: true },
- { id: 4, role: "viewer", name: "QA Viewers", visible: true },
- { id: 5, role: "member", name: "Design Team", visible: true },
- { id: 6, role: "owner", name: "Executive Board", visible: true },
- { id: 7, role: "menager", name: "HR Managers", visible: true },
- { id: 8, role: "viewer", name: "Marketing Reviewers", visible: false },
- { id: 9, role: "member", name: "Content Creators", visible: false },
- { id: 10, role: "menager", name: "Support Managers", visible: true },
- { id: 11, role: "viewer", name: "External Auditors", visible: false },
- { id: 12, role: "member", name: "Frontend Developers", visible: true },
- { id: 13, role: "member", name: "Backend Developers", visible: true },
- { id: 14, role: "viewer", name: "Guest Access", visible: false },
- { id: 15, role: "menager", name: "Operations", visible: true },
- ];
+ // Берём группы из стора
+ const groups = useAppSelector((store) => store.groups.groups);
+
+ // Берём текущего пользователя
+ const currentUserName = useAppSelector((store) => store.auth.username);
useEffect(() => {
- dispatch(setMenuActivePage("groups"))
- }, []);
+ dispatch(setMenuActivePage('groups'));
+ dispatch(fetchMyGroups());
+ }, [dispatch]);
+
+ // Разделяем группы
+ const { managedGroups, currentGroups, hiddenGroups } = useMemo(() => {
+ if (!groups || !currentUserName) {
+ return { managedGroups: [], currentGroups: [], hiddenGroups: [] };
+ }
+
+ const managed: typeof groups = [];
+ const current: typeof groups = [];
+ const hidden: typeof groups = []; // пока пустые, без логики
+
+ groups.forEach((group) => {
+ const me = group.members.find(
+ (m) => m.username === currentUserName,
+ );
+ if (!me) return;
+
+ if (me.role === 'Administrator') {
+ managed.push(group);
+ } else {
+ current.push(group);
+ }
+ });
+
+ return {
+ managedGroups: managed,
+ currentGroups: current,
+ hiddenGroups: hidden,
+ };
+ }, [groups, currentUserName]);
return (
-
+
-
-
+
Группы
{ }}
+ onClick={() => {
+ setModalActive(true);
+ }}
text="Создать группу"
className="absolute right-0"
/>
-
-
-
-
v.visible && (v.role == "owner" || v.role == "menager"))} />
- v.visible && (v.role == "member" || v.role == "viewer"))} />
- v.visible == false)} />
+
+
+
+
+
+
);
};
diff --git a/src/views/home/groups/GroupsBlock.tsx b/src/views/home/groups/GroupsBlock.tsx
index 05e7e76..e66d713 100644
--- a/src/views/home/groups/GroupsBlock.tsx
+++ b/src/views/home/groups/GroupsBlock.tsx
@@ -1,54 +1,72 @@
-import { useState, FC } from "react";
-import GroupItem from "./GroupItem";
-import { cn } from "../../../lib/cn";
-import { ChevroneDown } from "../../../assets/icons/groups";
-
-
-export interface Group {
- id: number;
- role: "menager" | "member" | "owner" | "viewer";
- visible: boolean;
- name: string;
-}
+import { useState, FC } from 'react';
+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';
interface GroupsBlockProps {
groups: Group[];
title: string;
className?: string;
+ setUpdateActive: (value: any) => void;
+ setUpdateGroup: (value: GroupUpdate) => void;
}
-
-const GroupsBlock: FC
= ({ groups, title, className }) => {
-
-
- const [active, setActive] = useState(title != "Скрытые");
-
+const GroupsBlock: FC = ({
+ groups,
+ title,
+ className,
+ setUpdateActive,
+ setUpdateGroup,
+}) => {
+ const [active, setActive] = useState(title != 'Скрытые');
return (
-
-
-
+
{
- setActive(!active)
- }}>
+ setActive(!active);
+ }}
+ >
{title}
-

+
-
+
-
- {
- groups.map((v, i) => )
- }
+ {groups.map((v, i) => (
+
+ ))}
diff --git a/src/views/home/groups/ModalCreate.tsx b/src/views/home/groups/ModalCreate.tsx
new file mode 100644
index 0000000..458c491
--- /dev/null
+++ b/src/views/home/groups/ModalCreate.tsx
@@ -0,0 +1,78 @@
+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';
+
+interface ModalCreateProps {
+ active: boolean;
+ setActive: (value: boolean) => void;
+}
+
+const ModalCreate: FC
= ({ active, setActive }) => {
+ const [name, setName] = useState('');
+ const [description, setDescription] = useState('');
+ const status = useAppSelector((state) => state.groups.statuses.create);
+ const dispatch = useAppDispatch();
+
+ useEffect(() => {
+ if (status == 'successful') {
+ setActive(false);
+ }
+ }, [status]);
+
+ return (
+
+
+
Создать группу
+
{
+ setName(v);
+ }}
+ placeholder="login"
+ />
+
{
+ setDescription(v);
+ }}
+ placeholder="login"
+ />
+
+
+
{
+ dispatch(createGroup({ name, description }));
+ }}
+ text="Создать"
+ disabled={status == 'loading'}
+ />
+ {
+ setActive(false);
+ }}
+ text="Отмена"
+ />
+
+
+
+ );
+};
+
+export default ModalCreate;
diff --git a/src/views/home/groups/ModalUpdate.tsx b/src/views/home/groups/ModalUpdate.tsx
new file mode 100644
index 0000000..9233c9f
--- /dev/null
+++ b/src/views/home/groups/ModalUpdate.tsx
@@ -0,0 +1,112 @@
+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 { deleteGroup, updateGroup } from '../../../redux/slices/groups';
+
+interface ModalUpdateProps {
+ active: boolean;
+ setActive: (value: boolean) => void;
+ groupId: number;
+ groupName: string;
+ groupDescription: string;
+}
+
+const ModalUpdate: FC = ({
+ active,
+ setActive,
+ groupName,
+ groupId,
+ groupDescription,
+}) => {
+ const [name, setName] = useState('');
+ const [description, setDescription] = useState('');
+ const statusUpdate = useAppSelector(
+ (state) => state.groups.statuses.update,
+ );
+ const statusDelete = useAppSelector(
+ (state) => state.groups.statuses.delete,
+ );
+ const dispatch = useAppDispatch();
+
+ useEffect(() => {
+ if (statusUpdate == 'successful') {
+ setActive(false);
+ }
+ }, [statusUpdate]);
+
+ useEffect(() => {
+ if (statusDelete == 'successful') {
+ setActive(false);
+ }
+ }, [statusDelete]);
+
+ return (
+
+
+
+ Изменить группу {groupName} #{groupId}
+
+
{
+ setName(v);
+ }}
+ placeholder="login"
+ />
+
{
+ setDescription(v);
+ }}
+ placeholder="login"
+ defaultState={groupDescription}
+ />
+
+
+
{
+ dispatch(deleteGroup(groupId));
+ }}
+ text="Удалить"
+ disabled={statusDelete == 'loading'}
+ color="error"
+ />
+ {
+ dispatch(
+ updateGroup({ name, description, groupId }),
+ );
+ }}
+ text="Обновить"
+ disabled={statusUpdate == 'loading'}
+ />
+ {
+ setActive(false);
+ }}
+ text="Отмена"
+ />
+
+
+
+ );
+};
+
+export default ModalUpdate;
diff --git a/src/views/home/menu/Menu.tsx b/src/views/home/menu/Menu.tsx
index 0e5cab3..e7b6ce4 100644
--- a/src/views/home/menu/Menu.tsx
+++ b/src/views/home/menu/Menu.tsx
@@ -1,29 +1,63 @@
-import { Logo } from "../../../assets/logos";
-import {Account, Clipboard, Cup, Home, Openbook, Users} from "../../../assets/icons/menu";
-import MenuItem from "./MenuItem";
-import { useAppSelector } from "../../../redux/hooks";
+import { Logo } from '../../../assets/logos';
+import {
+ Account,
+ Clipboard,
+ Cup,
+ Home,
+ Openbook,
+ Users,
+} from '../../../assets/icons/menu';
+import MenuItem from './MenuItem';
+import { useAppSelector } from '../../../redux/hooks';
const Menu = () => {
- const menuItems = [
- {text: "Главная", href: "/home", icon: Home, page: "home" },
- {text: "Задачи", href: "/home/missions", icon: Clipboard, page: "missions" },
- {text: "Статьи", href: "/home/articles", icon: Openbook, page: "articles" },
- {text: "Группы", href: "/home/groups", icon: Users, page: "groups" },
- {text: "Контесты", href: "/home/contests", icon: Cup, page: "contests" },
- {text: "Аккаунт", href: "/home/account", icon: Account, page: "account" },
- ];
- const activePage = useAppSelector((state) => state.store.menu.activePage);
+ const menuItems = [
+ { text: 'Главная', href: '/home', icon: Home, page: 'home' },
+ {
+ text: 'Задачи',
+ href: '/home/missions',
+ icon: Clipboard,
+ page: 'missions',
+ },
+ {
+ text: 'Статьи',
+ href: '/home/articles',
+ icon: Openbook,
+ page: 'articles',
+ },
+ { text: 'Группы', href: '/home/groups', icon: Users, page: 'groups' },
+ {
+ text: 'Контесты',
+ href: '/home/contests',
+ icon: Cup,
+ page: 'contests',
+ },
+ {
+ text: 'Аккаунт',
+ href: '/home/account',
+ icon: Account,
+ page: 'account',
+ },
+ ];
+ const activePage = useAppSelector((state) => state.store.menu.activePage);
- return (
-
-

-
- {menuItems.map((v, i) => (
-
-
- );
+ return (
+
+

+
+ {menuItems.map((v, i) => (
+
+ ))}
+
+
+ );
};
export default Menu;
diff --git a/src/views/home/menu/MenuItem.tsx b/src/views/home/menu/MenuItem.tsx
index d74c7fa..db112df 100644
--- a/src/views/home/menu/MenuItem.tsx
+++ b/src/views/home/menu/MenuItem.tsx
@@ -1,37 +1,44 @@
-import React from "react";
-import { Link } from "react-router-dom";
-import { useAppDispatch } from "../../../redux/hooks";
-import { setMenuActivePage } from "../../../redux/slices/store";
+import React from 'react';
+import { Link } from 'react-router-dom';
+import { useAppDispatch } from '../../../redux/hooks';
+import { setMenuActivePage } from '../../../redux/slices/store';
interface MenuItemProps {
- icon: string; // SVG или любой JSX
- text: string;
- href: string;
- page: string;
- active?: boolean; // необязательный, по умолчанию false
+ icon: string; // SVG или любой JSX
+ text: string;
+ href: string;
+ page: string;
+ active?: boolean; // необязательный, по умолчанию false
}
-const MenuItem: React.FC = ({ icon, text = "", href = "", active = false, page = "" }) => {
- const dispatch = useAppDispatch();
+const MenuItem: React.FC = ({
+ icon,
+ text = '',
+ href = '',
+ active = false,
+ page = '',
+}) => {
+ const dispatch = useAppDispatch();
- return (
- dispatch(setMenuActivePage(page))
- }
- >
-
- {text}
-
- );
+ onClick={() => dispatch(setMenuActivePage(page))}
+ >
+
+ {text}
+
+ );
};
export default MenuItem;
diff --git a/src/views/home/missions/MissionItem.tsx b/src/views/home/missions/MissionItem.tsx
index 2bdf6f8..3d4b6cc 100644
--- a/src/views/home/missions/MissionItem.tsx
+++ b/src/views/home/missions/MissionItem.tsx
@@ -1,71 +1,78 @@
-import { cn } from "../../../lib/cn";
-import { IconError, IconSuccess } from "../../../assets/icons/missions";
-import { useNavigate } from "react-router-dom";
+import { cn } from '../../../lib/cn';
+import { IconError, IconSuccess } from '../../../assets/icons/missions';
+import { useNavigate } from 'react-router-dom';
export interface MissionItemProps {
id: number;
- authorId: number;
+ authorId?: number;
name: string;
- difficulty: "Easy" | "Medium" | "Hard";
- tags: string[];
- timeLimit: number;
- memoryLimit: number;
- createdAt: string;
- updatedAt: string;
- type: "first" | "second";
- status: "empty" | "success" | "error";
+ difficulty: 'Easy' | 'Medium' | 'Hard';
+ 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`;
+ 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 megabytes = Math.floor(bytes / (1024 * 1024));
+ return `${megabytes} МБ`;
}
const MissionItem: React.FC = ({
- id, name, difficulty, timeLimit, memoryLimit, type, status
+ id,
+ name,
+ difficulty,
+ timeLimit = 1000,
+ memoryLimit = 256 * 1024 * 1024,
+ type,
+ status,
}) => {
const navigate = useNavigate();
return (
- {navigate(`/mission/${id}`)}}
+
{
+ navigate(`/mission/${id}`);
+ }}
>
-
- #{id}
-
-
- {name}
-
+
#{id}
+
{name}
- стандартный ввод/вывод {formatMilliseconds(timeLimit)}, {formatBytesToMB(memoryLimit)}
+ стандартный ввод/вывод {formatMilliseconds(timeLimit)},{' '}
+ {formatBytesToMB(memoryLimit)}
-
+
{difficulty}
- {
- status == "error" &&

- }
- {
- status == "success" &&

- }
+ {status == 'error' &&

}
+ {status == 'success' &&

}
);
diff --git a/src/views/home/missions/Missions.tsx b/src/views/home/missions/Missions.tsx
index c62b0db..3b5203e 100644
--- a/src/views/home/missions/Missions.tsx
+++ b/src/views/home/missions/Missions.tsx
@@ -1,17 +1,16 @@
-import MissionItem from "./MissionItem";
-import { SecondaryButton } from "../../../components/button/SecondaryButton";
-import { useAppDispatch, useAppSelector } from "../../../redux/hooks";
-import { useEffect } from "react";
-import { setMenuActivePage } from "../../../redux/slices/store";
-import { useNavigate } from "react-router-dom";
-import { fetchMissions } from "../../../redux/slices/missions";
-
+import MissionItem from './MissionItem';
+import { SecondaryButton } from '../../../components/button/SecondaryButton';
+import { useAppDispatch, useAppSelector } from '../../../redux/hooks';
+import { useEffect, useState } from 'react';
+import { setMenuActivePage } from '../../../redux/slices/store';
+import { fetchMissions } from '../../../redux/slices/missions';
+import ModalCreate from './ModalCreate';
export interface Mission {
id: number;
authorId: number;
name: string;
- difficulty: "Easy" | "Medium" | "Hard";
+ difficulty: 'Easy' | 'Medium' | 'Hard';
tags: string[];
timeLimit: number;
memoryLimit: number;
@@ -20,61 +19,63 @@ export interface Mission {
}
const Missions = () => {
-
const dispatch = useAppDispatch();
- const naivgate = useNavigate();
+ const [modalActive, setModalActive] = useState
(false);
const missions = useAppSelector((state) => state.missions.missions);
useEffect(() => {
- dispatch(setMenuActivePage("missions"))
- dispatch(fetchMissions({}))
+ dispatch(setMenuActivePage('missions'));
+ dispatch(fetchMissions({}));
}, []);
-
return (
-
Задачи
-
{naivgate("/upload")}}
- text="Создать задачу"
+ {
+ setModalActive(true);
+ }}
+ text="Добавить задачу"
className="absolute right-0"
/>
-
-
-
+
-
- {missions.map((v, i) => (
-
- ))}
+ {missions.map((v, i) => (
+
+ ))}
-
-
- pages
-
+
pages
+
+
);
};
diff --git a/src/views/home/missions/ModalCreate.tsx b/src/views/home/missions/ModalCreate.tsx
new file mode 100644
index 0000000..e93fce6
--- /dev/null
+++ b/src/views/home/missions/ModalCreate.tsx
@@ -0,0 +1,169 @@
+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 {
+ setMissionsStatus,
+ uploadMission,
+} from '../../../redux/slices/missions';
+
+interface ModalCreateProps {
+ active: boolean;
+ setActive: (value: boolean) => void;
+}
+
+const ModalCreate: FC = ({ active, setActive }) => {
+ const [name, setName] = useState('');
+ const [difficulty, setDifficulty] = useState(1);
+ const [file, setFile] = useState(null);
+ const [tagInput, setTagInput] = useState('');
+ const [tags, setTags] = useState([]);
+
+ const status = useAppSelector((state) => state.missions.statuses.upload);
+ const dispatch = useAppDispatch();
+
+ const addTag = () => {
+ const newTag = tagInput.trim();
+ if (newTag && !tags.includes(newTag)) {
+ setTags([...tags, newTag]);
+ setTagInput('');
+ }
+ };
+
+ const removeTag = (tagToRemove: string) => {
+ setTags(tags.filter((tag) => tag !== tagToRemove));
+ };
+
+ const handleFileChange = (e: React.ChangeEvent) => {
+ if (e.target.files && e.target.files[0]) {
+ setFile(e.target.files[0]);
+ }
+ };
+
+ const handleSubmit = async () => {
+ if (!file) return alert('Выберите файл миссии!');
+ dispatch(uploadMission({ file, name, difficulty, tags }));
+ };
+
+ useEffect(() => {
+ if (status === 'successful') {
+ alert('Миссия успешно загружена!');
+ setName('');
+ setDifficulty(1);
+ setTags([]);
+ setFile(null);
+ dispatch(setMissionsStatus({ key: 'upload', status: 'idle' }));
+ setActive(false);
+ }
+ }, [status]);
+
+ useEffect(() => {
+ dispatch(setMissionsStatus({ key: 'upload', status: 'idle' }));
+ }, [active]);
+
+ return (
+
+
+
Добавить задачу
+
+
+
+
setDifficulty(Number(v))}
+ placeholder="1"
+ />
+
+
+
+
+
+
+ {/* Теги */}
+
+
+
setTagInput(v)}
+ defaultState={tagInput}
+ placeholder="arrays"
+ onKeyDown={(e) => {
+ if (e.key === 'Enter') addTag();
+ }}
+ />
+
+
+
+ {tags.map((tag) => (
+
+ {tag}
+ removeTag(tag)}
+ className="text-liquid-red font-bold ml-[5px]"
+ >
+ ×
+
+
+ ))}
+
+
+
+
+
+
setActive(false)}
+ text="Отмена"
+ />
+
+
+ {status == 'failed' &&
error
}
+
+
+ );
+};
+
+export default ModalCreate;
diff --git a/src/views/mission/UploadMissionForm.tsx b/src/views/mission/UploadMissionForm.tsx
deleted file mode 100644
index f5ea22f..0000000
--- a/src/views/mission/UploadMissionForm.tsx
+++ /dev/null
@@ -1,101 +0,0 @@
-import React, { useState } from "react";
-import { useAppDispatch, useAppSelector } from "../../redux/hooks";
-import { uploadMission } from "../../redux/slices/missions";
-
-const UploadMissionForm: React.FC = () => {
- const dispatch = useAppDispatch();
- const { status, error } = useAppSelector(state => state.missions);
-
- // Локальные состояния формы
- const [name, setName] = useState("");
- const [difficulty, setDifficulty] = useState(1);
- const [tags, setTags] = useState([]);
- const [tagsValue, setTagsValue] = useState("");
- const [file, setFile] = useState(null);
-
- const handleSubmit = async (e: React.FormEvent) => {
- e.preventDefault();
- if (!file) return alert("Выберите файл миссии!");
-
- try {
- dispatch(uploadMission({ file, name, difficulty, tags }));
-
- alert("Миссия успешно загружена!");
- setName("");
- setDifficulty(1);
- setTags([]);
- setFile(null);
- } catch (err) {
- console.error(err);
- alert("Ошибка при загрузке миссии: " + err);
- }
- };
-
- const handleFileChange = (e: React.ChangeEvent) => {
- if (e.target.files && e.target.files[0]) {
- setFile(e.target.files[0]);
- }
- };
-
- const handleTagsChange = (e: React.ChangeEvent) => {
- setTagsValue(e.target.value);
- const value = e.target.value;
- const tagsArray = value.split(",").map(tag => tag.trim()).filter(tag => tag);
- setTags(tagsArray);
- };
-
- return (
-
- );
-};
-
-export default UploadMissionForm;
diff --git a/src/views/mission/codeeditor/CodeEditor.tsx b/src/views/mission/codeeditor/CodeEditor.tsx
index a9133d9..c3c7f4a 100644
--- a/src/views/mission/codeeditor/CodeEditor.tsx
+++ b/src/views/mission/codeeditor/CodeEditor.tsx
@@ -1,141 +1,153 @@
-import React, { useEffect, useState } from "react";
-import Editor from "@monaco-editor/react";
-import { upload } from "../../../assets/icons/input";
-import { cn } from "../../../lib/cn";
-import { DropDownList } from "../../../components/drop-down-list/DropDownList";
+import React, { useEffect, useState } from 'react';
+import Editor from '@monaco-editor/react';
+import { upload } from '../../../assets/icons/input';
+import { cn } from '../../../lib/cn';
+import { DropDownList } from '../../../components/drop-down-list/DropDownList';
const languageMap: Record = {
- c: "cpp",
- cpp: "cpp",
- java: "java",
- python: "python",
- pascal: "pascal",
- kotlin: "kotlin",
- csharp: "csharp"
+ c: 'cpp',
+ 'C++': 'cpp',
+ java: 'java',
+ python: 'python',
+ pascal: 'pascal',
+ kotlin: 'kotlin',
+ csharp: 'csharp',
};
-export interface CodeEditorProps {
+export interface CodeEditorProps {
onChange: (value: string) => void;
onChangeLanguage: (value: string) => void;
}
-const CodeEditor: React.FC = ({onChange, onChangeLanguage}) => {
- const [language, setLanguage] = useState("C++");
- const [code, setCode] = useState("");
- const [isDragging, setIsDragging] = useState(false);
+const CodeEditor: React.FC = ({
+ onChange,
+ onChangeLanguage,
+}) => {
+ const [language, setLanguage] = useState('C++');
+ const [code, setCode] = useState('');
+ const [isDragging, setIsDragging] = useState(false);
+ const items = [
+ { value: 'c', text: 'C' },
+ { value: 'C++', text: 'C++' },
+ { value: 'java', text: 'Java' },
+ { value: 'python', text: 'Python' },
+ { value: 'pascal', text: 'Pascal' },
+ { value: 'kotlin', text: 'Kotlin' },
+ { value: 'csharp', text: 'C#' },
+ ];
- const items = [
- { value: "c", text: "C" },
- { value: "C++", text: "C++" },
- { value: "java", text: "Java" },
- { value: "python", text: "Python" },
- { value: "pascal", text: "Pascal" },
- { value: "kotlin", text: "Kotlin" },
- { value: "csharp", text: "C#" },
- ];
+ useEffect(() => {
+ onChange(code);
+ }, [code]);
+ useEffect(() => {
+ onChangeLanguage(language);
+ }, [language]);
- useEffect(() => {
- onChange(code);
- }, [code])
- useEffect(() => {
- onChangeLanguage(language);
- }, [language])
+ const handleFileUpload = (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0];
+ if (!file) return;
- const handleFileUpload = (e: React.ChangeEvent) => {
- const file = e.target.files?.[0];
- if (!file) return;
-
- const reader = new FileReader();
- reader.onload = (event) => {
- const text = event.target?.result;
- if (typeof text === "string") setCode(text);
+ const reader = new FileReader();
+ reader.onload = (event) => {
+ const text = event.target?.result;
+ if (typeof text === 'string') setCode(text);
+ };
+ reader.readAsText(file);
+ e.target.value = '';
};
- reader.readAsText(file);
- e.target.value = "";
- };
- const handleDrop = (e: React.DragEvent) => {
- e.preventDefault();
- setIsDragging(false);
- const droppedFile = e.dataTransfer.files[0];
- if (!droppedFile) return;
+ const handleDrop = (e: React.DragEvent) => {
+ e.preventDefault();
+ setIsDragging(false);
+ const droppedFile = e.dataTransfer.files[0];
+ if (!droppedFile) return;
- const reader = new FileReader();
- reader.onload = (event) => {
- const text = event.target?.result;
- if (typeof text === "string") setCode(text);
+ const reader = new FileReader();
+ reader.onload = (event) => {
+ const text = event.target?.result;
+ if (typeof text === 'string') setCode(text);
+ };
+ reader.readAsText(droppedFile);
};
- reader.readAsText(droppedFile);
- };
- const handleDragOver = (e: React.DragEvent) => {
- e.preventDefault(); // обязательно
- };
+ const handleDragOver = (e: React.DragEvent) => {
+ e.preventDefault(); // обязательно
+ };
- const handleDragEnter = (e: React.DragEvent) => {
- e.preventDefault();
- setIsDragging(true);
- };
+ const handleDragEnter = (e: React.DragEvent) => {
+ e.preventDefault();
+ setIsDragging(true);
+ };
- const handleDragLeave = (e: React.DragEvent) => {
- e.preventDefault();
- setIsDragging(false);
- };
+ const handleDragLeave = (e: React.DragEvent) => {
+ e.preventDefault();
+ setIsDragging(false);
+ };
- return (
-
- {/* Панель выбора языка и загрузки файла */}
-
-
-
{ setLanguage(v) }} defaultState={{ value: "C++", text: "C++" }}/>
+ return (
+
+ {/* Панель выбора языка и загрузки файла */}
+
+
+ {/* Monaco Editor */}
+
+ setCode(value ?? '')}
+ theme="vs-dark"
+ options={{
+ fontSize: 14,
+ minimap: { enabled: false },
+ automaticLayout: true,
+ quickSuggestions: true,
+ suggestOnTriggerCharacters: true,
+ tabSize: 4,
+ insertSpaces: true,
+ detectIndentation: false,
+ autoIndent: 'full',
+ }}
+ />
+
-
-
- {/* Monaco Editor */}
-
- setCode(value ?? "")}
- theme="vs-dark"
- options={{
- fontSize: 14,
- minimap: { enabled: false },
- automaticLayout: true,
- quickSuggestions: true,
- suggestOnTriggerCharacters: true,
- tabSize: 4,
- insertSpaces: true,
- detectIndentation: false,
- autoIndent: "full",
- }}
- />
-
-
- );
+ );
};
export default CodeEditor;
diff --git a/src/views/mission/statement/Header.tsx b/src/views/mission/statement/Header.tsx
index 35a110a..979957e 100644
--- a/src/views/mission/statement/Header.tsx
+++ b/src/views/mission/statement/Header.tsx
@@ -1,28 +1,64 @@
-import React from "react";
-import { chevroneLeft, chevroneRight, arrowLeft } from "../../../assets/icons/header";
-import { Logo } from "../../../assets/logos";
-import { useNavigate } from "react-router-dom";
+import React from 'react';
+import {
+ chevroneLeft,
+ chevroneRight,
+ arrowLeft,
+} from '../../../assets/icons/header';
+import { Logo } from '../../../assets/logos';
+import { useNavigate } from 'react-router-dom';
interface HeaderProps {
missionId: number;
+ back?: string;
}
-const Header: React.FC
= ({
- missionId
-}) => {
+const Header: React.FC = ({ missionId, back }) => {
const navigate = useNavigate();
return (
-
{ navigate("/home") }} />
+
{
+ navigate('/home');
+ }}
+ />
-
{ navigate("/home/missions") }} />
+
{
+ if (back) navigate(back);
+ else navigate('/home/missions');
+ }}
+ />
-

{ navigate(`/mission/${missionId - 1}`) }} />
+

{
+ if (missionId <= 1) return;
+ if (back)
+ navigate(`/mission/${missionId - 1}?back=${back}`);
+ else navigate(`/mission/${missionId - 1}`);
+ }}
+ />
{missionId}
-

{ navigate(`/mission/${missionId + 1}`) }} />
+

{
+ if (back)
+ navigate(`/mission/${missionId + 1}?back=${back}`);
+ else navigate(`/mission/${missionId + 1}`);
+ }}
+ />
-
);
};
diff --git a/src/views/mission/statement/LaTextContainer.tsx b/src/views/mission/statement/LaTextContainer.tsx
index 1309240..1d58686 100644
--- a/src/views/mission/statement/LaTextContainer.tsx
+++ b/src/views/mission/statement/LaTextContainer.tsx
@@ -1,113 +1,131 @@
-import { useEffect, useRef, useState } from "react";
+import { useEffect, useRef, useState } from 'react';
declare global {
- interface Window {
- MathJax?: {
- startup?: { promise?: Promise };
- typesetPromise?: (elements?: Element[]) => Promise;
- [key: string]: any;
- };
- }
+ interface Window {
+ MathJax?: {
+ startup?: { promise?: Promise };
+ typesetPromise?: (elements?: Element[]) => Promise;
+ [key: string]: any;
+ };
+ }
}
interface MediaFile {
- id: number;
- fileName: string;
- mediaUrl: string;
+ id: number;
+ fileName: string;
+ mediaUrl: string;
}
interface LaTextContainerProps {
- html: string;
- latex: string;
- mediaFiles?: MediaFile[];
+ html: string;
+ latex: string;
+ mediaFiles?: MediaFile[];
}
let mathJaxPromise: Promise | null = null;
const loadMathJax = () => {
- if (mathJaxPromise) return mathJaxPromise;
+ if (mathJaxPromise) return mathJaxPromise;
- mathJaxPromise = new Promise((resolve, reject) => {
- if (window.MathJax?.typesetPromise) {
- resolve();
- return;
- }
+ mathJaxPromise = new Promise((resolve, reject) => {
+ if (window.MathJax?.typesetPromise) {
+ resolve();
+ return;
+ }
- (window as any).MathJax = {
- tex: {
- inlineMath: [["$$$", "$$$"]],
- displayMath: [["$$$$$$", "$$$$$$"]],
- processEscapes: true,
- },
- options: {
- skipHtmlTags: ["script", "noscript", "style", "textarea", "pre", "code"],
- },
- startup: { typeset: false },
- };
+ (window as any).MathJax = {
+ tex: {
+ inlineMath: [['$$$', '$$$']],
+ displayMath: [['$$$$$$', '$$$$$$']],
+ processEscapes: true,
+ },
+ options: {
+ skipHtmlTags: [
+ 'script',
+ 'noscript',
+ 'style',
+ 'textarea',
+ 'pre',
+ 'code',
+ ],
+ },
+ startup: { typeset: false },
+ };
- const script = document.createElement("script");
- script.id = "mathjax-script";
- script.src = "https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js";
- script.async = true;
+ const script = document.createElement('script');
+ script.id = 'mathjax-script';
+ script.src =
+ 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js';
+ script.async = true;
- script.onload = () => {
- window.MathJax?.startup?.promise?.then(resolve).catch(reject);
- };
+ script.onload = () => {
+ window.MathJax?.startup?.promise?.then(resolve).catch(reject);
+ };
- script.onerror = reject;
- document.head.appendChild(script);
- });
+ script.onerror = reject;
+ document.head.appendChild(script);
+ });
- return mathJaxPromise;
+ return mathJaxPromise;
};
-const replaceImages = (html: string, latex: string, mediaFiles?: MediaFile[]) => {
- const parser = new DOMParser();
- const doc = parser.parseFromString(html, "text/html");
+const replaceImages = (
+ html: string,
+ latex: string,
+ mediaFiles?: MediaFile[],
+) => {
+ const parser = new DOMParser();
+ const doc = parser.parseFromString(html, 'text/html');
- const latexImageNames = Array.from(latex.matchAll(/\\includegraphics\{(.+?)\}/g)).map(
- (match) => match[1]
- );
+ const latexImageNames = Array.from(
+ latex.matchAll(/\\includegraphics\{(.+?)\}/g),
+ ).map((match) => match[1]);
- const imgs = doc.querySelectorAll("img.tex-graphics");
+ const imgs = doc.querySelectorAll('img.tex-graphics');
- imgs.forEach((img, idx) => {
- const imageName = latexImageNames[idx];
- if (!imageName || !mediaFiles) return;
- const mediaFile = mediaFiles.find((f) => f.fileName === imageName);
- if (mediaFile) img.src = mediaFile.mediaUrl;
- });
+ imgs.forEach((img, idx) => {
+ const imageName = latexImageNames[idx];
+ if (!imageName || !mediaFiles) return;
+ const mediaFile = mediaFiles.find((f) => f.fileName === imageName);
+ if (mediaFile) img.src = mediaFile.mediaUrl;
+ });
- return doc.body.innerHTML;
+ return doc.body.innerHTML;
};
-const LaTextContainer: React.FC = ({ html, latex, mediaFiles }) => {
- const containerRef = useRef(null);
- const [processedHtml, setProcessedHtml] = useState(html);
+const LaTextContainer: React.FC = ({
+ html,
+ latex,
+ mediaFiles,
+}) => {
+ const containerRef = useRef(null);
+ const [processedHtml, setProcessedHtml] = useState(html);
- // 1️⃣ Обновляем HTML при изменении входных данных
- useEffect(() => {
- setProcessedHtml(replaceImages(html, latex, mediaFiles));
- }, [html, latex, mediaFiles]);
+ // 1️⃣ Обновляем HTML при изменении входных данных
+ useEffect(() => {
+ setProcessedHtml(replaceImages(html, latex, mediaFiles));
+ }, [html, latex, mediaFiles]);
- // 2️⃣ После рендера обновленного HTML применяем MathJax
- useEffect(() => {
- const renderMath = () => {
- if (containerRef.current && window.MathJax?.typesetPromise) {
- window.MathJax.typesetPromise([containerRef.current]).catch(console.error);
- }
- };
+ // 2️⃣ После рендера обновленного HTML применяем MathJax
+ useEffect(() => {
+ const renderMath = () => {
+ if (containerRef.current && window.MathJax?.typesetPromise) {
+ window.MathJax.typesetPromise([containerRef.current]).catch(
+ console.error,
+ );
+ }
+ };
- loadMathJax().then(renderMath).catch(console.error);
- }, [processedHtml]); // 👈 ключевой момент — триггерим именно по processedHtml
+ loadMathJax().then(renderMath).catch(console.error);
+ }, [processedHtml]); // 👈 ключевой момент — триггерим именно по processedHtml
- return (
-
- );
+ return (
+
+ );
};
export default LaTextContainer;
diff --git a/src/views/mission/statement/MissionSubmissions.tsx b/src/views/mission/statement/MissionSubmissions.tsx
index e058ee1..cdd725d 100644
--- a/src/views/mission/statement/MissionSubmissions.tsx
+++ b/src/views/mission/statement/MissionSubmissions.tsx
@@ -1,17 +1,12 @@
-import SubmissionItem from "./SubmissionItem";
-import { SecondaryButton } from "../../../components/button/SecondaryButton";
-import { useAppDispatch, useAppSelector } from "../../../redux/hooks";
-import { FC, useEffect } from "react";
-import { setMenuActivePage } from "../../../redux/slices/store";
-import { useNavigate } from "react-router-dom";
-import { fetchMissions } from "../../../redux/slices/missions";
-
+import SubmissionItem from './SubmissionItem';
+import { useAppSelector } from '../../../redux/hooks';
+import { FC, useEffect } from 'react';
export interface Mission {
id: number;
authorId: number;
name: string;
- difficulty: "Easy" | "Medium" | "Hard";
+ difficulty: 'Easy' | 'Medium' | 'Hard';
tags: string[];
timeLimit: number;
memoryLimit: number;
@@ -19,39 +14,45 @@ export interface Mission {
updatedAt: string;
}
-interface MissionSubmissionsProps{
+interface MissionSubmissionsProps {
missionId: number;
}
-const MissionSubmissions: FC = ({missionId}) => {
- const submissions = useAppSelector((state) => state.submin.submitsById[missionId]);
-
- useEffect(() => {
-
- }, []);
+const MissionSubmissions: FC = ({ missionId }) => {
+ const submissions = useAppSelector(
+ (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;
- }
+ };
return (
-
-
- {submissions && submissions.map((v, i) => (
- (
+
))}
diff --git a/src/views/mission/statement/Statement.tsx b/src/views/mission/statement/Statement.tsx
index 63449fc..e313c62 100644
--- a/src/views/mission/statement/Statement.tsx
+++ b/src/views/mission/statement/Statement.tsx
@@ -1,8 +1,48 @@
-import React from "react";
-// import { cn } from "../../../lib/cn";
-import LaTextContainer from "./LaTextContainer";
+import React, { FC } from 'react';
+import { cn } from '../../../lib/cn';
+import LaTextContainer from './LaTextContainer';
+import { CopyIcon } from '../../../assets/icons/missions';
// import FullLatexRenderer from "./FullLatexRenderer";
+import { useState } from 'react';
+
+interface CopyableDivPropd {
+ content: string;
+}
+
+const CopyableDiv: FC = ({ content }) => {
+ const [hovered, setHovered] = useState(false);
+
+ const handleCopy = async () => {
+ try {
+ await navigator.clipboard.writeText(content);
+ alert('Скопировано!');
+ } catch (err) {
+ console.error('Ошибка копирования:', err);
+ }
+ };
+
+ return (
+ setHovered(true)}
+ onMouseLeave={() => setHovered(false)}
+ >
+ {content}
+
+

+
+ );
+};
+
export interface StatementData {
id?: number;
name?: string;
@@ -19,10 +59,10 @@ export interface StatementData {
}
function extractDivByClass(html: string, className: string): string {
- const parser = new DOMParser();
- const doc = parser.parseFromString(html, "text/html");
- const div = doc.querySelector(`div.${className}`);
- return div ? div.outerHTML : "";
+ const parser = new DOMParser();
+ const doc = parser.parseFromString(html, 'text/html');
+ const div = doc.querySelector(`div.${className}`);
+ return div ? div.outerHTML : '';
}
const Statement: React.FC = ({
@@ -31,63 +71,110 @@ const Statement: React.FC = ({
tags,
timeLimit = 1000,
memoryLimit = 256 * 1024 * 1024,
- legend = "",
- input = "",
- output = "",
+ legend = '',
+ input = '',
+ output = '',
sampleTests = [],
- notes = "",
- html = "",
+ notes = '',
+ html = '',
mediaFiles,
}) => {
-
return (
-
{name}
-
Задача #{id}
+
+ {name}
+
+
+ Задача #{id}
+
- {tags && tags.map((v, i) =>
{v}
)}
+ {tags &&
+ tags.map((v, i) => (
+
+ {v}
+
+ ))}
-
ограничение по времени на тест: {timeLimit / 1000} секунда
-
ограничение по памяти на тест: {memoryLimit / 1024 / 1024} мегабайт
-
ввод: стандартный ввод
-
вывод: стандартный вывод
+
+
+ ограничение по времени на тест:
+ {' '}
+ {timeLimit / 1000} секунда
+
+
+
+ ограничение по памяти на тест:
+ {' '}
+ {memoryLimit / 1024 / 1024} мегабайт
+
+
+ ввод: стандартный
+ ввод
+
+
+ вывод:{' '}
+ стандартный вывод
+
-
+
-
+
-
+
-
{sampleTests.length == 1 ? "Пример" : "Примеры"}
-
+
+ {sampleTests.length == 1 ? 'Пример' : 'Примеры'}
+
- {sampleTests.map((v, i) =>
+ {sampleTests.map((v, i) => (
-
Входные данные
-
{v.input}
-
Выходные данные
-
{v.output}
+
+ Входные данные
+
+
+
+ Выходные данные
+
+
- )}
+ ))}
-
);
};
-export default Statement;
\ No newline at end of file
+export default Statement;
diff --git a/src/views/mission/statement/SubmissionItem.tsx b/src/views/mission/statement/SubmissionItem.tsx
index 49c215f..c4b86a3 100644
--- a/src/views/mission/statement/SubmissionItem.tsx
+++ b/src/views/mission/statement/SubmissionItem.tsx
@@ -1,14 +1,14 @@
-import { cn } from "../../../lib/cn";
-import { IconError, IconSuccess } from "../../../assets/icons/missions";
-import { useNavigate } from "react-router-dom";
+import { cn } from '../../../lib/cn';
+// import { IconError, IconSuccess } from "../../../assets/icons/missions";
+// import { useNavigate } from "react-router-dom";
export interface SubmissionItemProps {
id: number;
language: string;
time: string;
verdict: string;
- type: "first" | "second";
- status?: "success" | "wronganswer" | "timelimit";
+ type: 'first' | 'second';
+ status?: 'success' | 'wronganswer' | 'timelimit';
}
export function formatMilliseconds(ms: number): string {
@@ -23,16 +23,16 @@ export function formatBytesToMB(bytes: number): string {
}
function formatDate(dateString: string): string {
- const date = new Date(dateString);
+ 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 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");
+ const hours = date.getHours().toString().padStart(2, '0');
+ const minutes = date.getMinutes().toString().padStart(2, '0');
- return `${day}/${month}/${year}\n${hours}:${minutes}`;
+ return `${day}/${month}/${year}\n${hours}:${minutes}`;
}
const SubmissionItem: React.FC = ({
@@ -43,33 +43,37 @@ const SubmissionItem: React.FC = ({
type,
status,
}) => {
- const navigate = useNavigate();
+ // const navigate = useNavigate();
return (
- { }}
+
{}}
>
-
- #{id}
-
+
#{id}
{formatDate(time)}
-
- {language}
-
-
diff --git a/src/views/mission/submission/Submission.tsx b/src/views/mission/submission/Submission.tsx
new file mode 100644
index 0000000..e69de29
diff --git a/tailwind.config.js b/tailwind.config.js
index b758f44..1cbea2f 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -11,5 +11,6 @@ export default {
},
},
},
- plugins: [],
+ plugins: [require('@tailwindcss/typography')],
+
};
diff --git a/tsconfig.app.tsbuildinfo b/tsconfig.app.tsbuildinfo
index 67f9d00..dc32943 100644
--- a/tsconfig.app.tsbuildinfo
+++ b/tsconfig.app.tsbuildinfo
@@ -1 +1 @@
-{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/button/primarybutton.tsx","./src/components/button/secondarybutton.tsx","./src/components/checkbox/checkbox.tsx","./src/components/input/input.tsx","./src/components/switch/switch.tsx","./src/config/colors.ts","./src/lib/cn.ts","./src/redux/store.ts"],"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/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