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 ( - - ); + {/* Граница при выделении через tab */} +
+ {children || text} +
+
+ {children || text} +
+ + + ); }; 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); - }} - /> -
- - - {active && ( - + return ( + - -
- {variant == "label" && ( -
- {label} -
- )} -
- ); + > +
+ { + setActive(!active); + }} + /> +
+ + + {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 ( +
+
+ + onChange('startsAt', e.target.value)} + className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" + /> +
+
+ + onChange('endsAt', e.target.value)} + className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" + /> +
+
+ ); +}; + +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} +
+
+ { + setValue(e.target.value); + }} + onKeyDown={(e: React.KeyboardEvent) => { + if (onKeyDown) onKeyDown(e); + }} + /> + {type == 'password' && ( + { + setVIsible(!visible); + }} + /> + )} +
- 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); + }} + /> + ) : ( +
navigate(back ? back : '/home/articles')} + /> + )} + + {activeEditor ? ( + + ) : ( +
+
+ {refactor + ? `Редактирование статьи: \"${article?.name}\"` + : 'Создание статьи'} +
+
+ {refactor ? ( +
+ { + dispatch( + updateArticle({ + articleId, + name, + tags, + content: code, + }), + ); + }} + text="Обновить" + className="mt-[20px]" + disabled={statusUpdate == 'loading'} + /> + { + dispatch(deleteArticle(articleId)); + }} + color="error" + text="Удалить" + className="mt-[20px]" + disabled={statusDelete == 'loading'} + /> +
+ ) : ( + { + dispatch( + createArticle({ + name, + tags, + content: code, + }), + ); + }} + text="Опубликовать" + className="mt-[20px]" + disabled={statusCreate == 'loading'} + /> + )} +
+ + { + setName(v); + }} + placeholder="Новая статья" + /> + + {/* Блок для тегов */} +
+
+ { + setTagInput(v); + }} + defaultState={tagInput} + placeholder="arrays" + onKeyDown={(e) => { + console.log(e.key); + if (e.key == 'Enter') addTag(); + }} + /> + +
+
+ {tags.map((tag) => ( +
+ {tag} + +
+ ))} +
+
+ + setActiveEditor(true)} + text="Редактировать текст" + className="mt-[20px]" + /> + +
+ )} +
+ ); +}; + +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 ( +
+ Logo { + navigate('/home'); + }} + /> + + back { + navigate(back ? back : '/home/articles'); + }} + /> + +
+ back { + if (articleId <= 1) return; + + if (back) + navigate(`/article/${articleId - 1}?back=${back}`); + else navigate(`/article/${articleId - 1}`); + }} + /> + #{articleId} + back { + if (back) + navigate(`/article/${articleId + 1}?back=${back}`); + else navigate(`/article/${articleId + 1}`); + }} + /> +
+
+ ); +}; + +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-логотип: + +![Markdown Logo](https://upload.wikimedia.org/wikipedia/commons/4/48/Markdown-mark.svg) + +или + +\"img\"/ + +или если нужно выравнивание по центру + +
+ \"img\"/ +
+ + +--- + +## 🧠 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) + + `\"img\"/` + + markdown.slice(cursorPos); + + setMarkdown(newText); + } catch (err) { + console.error('Ошибка загрузки изображения:', err); + } + } + } + }; + + return ( +
+ {/* Предпросмотр */} +
+
+

+ 👀 Предпросмотр +

+ +
+
+ + {/* Редактор */} +
+
+

+ 📝 Редактор +

+