From ac41982f088eaca06625a9c2869573e393d8b0c6 Mon Sep 17 00:00:00 2001 From: Andrew Kalinin Date: Wed, 31 Dec 2025 02:02:25 +0300 Subject: [PATCH 01/23] feat: initializing the Redux store --- hwproj.front/package-lock.json | 177 +++++++++++++++++++++++--------- hwproj.front/package.json | 2 + hwproj.front/src/store/index.ts | 0 3 files changed, 130 insertions(+), 49 deletions(-) create mode 100644 hwproj.front/src/store/index.ts diff --git a/hwproj.front/package-lock.json b/hwproj.front/package-lock.json index a0882868d..fe2e3c324 100644 --- a/hwproj.front/package-lock.json +++ b/hwproj.front/package-lock.json @@ -20,6 +20,7 @@ "@mui/lab": "^5.0.0-alpha.99", "@mui/material": "^5.16.11", "@mui/x-charts": "^8.2.0", + "@reduxjs/toolkit": "^2.11.2", "@storybook/addon-knobs": "^6.3.0", "@types/bluebird": "^3.5.36", "@types/classnames": "^2.3.1", @@ -52,6 +53,7 @@ "react-drag-drop-files": "^3.1.0", "react-markdown": "^5.0.0", "react-query": "^3.21.1", + "react-redux": "^9.2.0", "react-router-dom": "^6.5.0", "react-social-login-buttons": "^3.5.1", "react-syntax-highlighter": "^15.5.0", @@ -4564,6 +4566,32 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@reduxjs/toolkit": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", + "integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, "node_modules/@remix-run/router": { "version": "1.23.0", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz", @@ -4874,6 +4902,18 @@ "dev": true, "license": "MIT" }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, "node_modules/@storybook/addon-knobs": { "version": "6.4.0", "resolved": "https://registry.npmjs.org/@storybook/addon-knobs/-/addon-knobs-6.4.0.tgz", @@ -4977,6 +5017,7 @@ "version": "6.5.16", "resolved": "https://registry.npmjs.org/@storybook/addons/-/addons-6.5.16.tgz", "integrity": "sha512-p3DqQi+8QRL5k7jXhXmJZLsE/GqHqyY6PcoA1oNTJr0try48uhTGUOYkgzmqtDaa/qPFO5LP+xCPzZXckGtquQ==", + "dev": true, "license": "MIT", "dependencies": { "@storybook/api": "6.5.16", @@ -5004,12 +5045,14 @@ "version": "0.13.11", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "dev": true, "license": "MIT" }, "node_modules/@storybook/api": { "version": "6.5.16", "resolved": "https://registry.npmjs.org/@storybook/api/-/api-6.5.16.tgz", "integrity": "sha512-HOsuT8iomqeTMQJrRx5U8nsC7lJTwRr1DhdD0SzlqL4c80S/7uuCy4IZvOt4sYQjOzW5fOo/kamcoBXyLproTA==", + "dev": true, "license": "MIT", "dependencies": { "@storybook/channels": "6.5.16", @@ -5043,6 +5086,7 @@ "version": "0.13.11", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "dev": true, "license": "MIT" }, "node_modules/@storybook/builder-webpack4": { @@ -5441,6 +5485,7 @@ "version": "6.5.16", "resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-6.5.16.tgz", "integrity": "sha512-VylzaWQZaMozEwZPJdyJoz+0jpDa8GRyaqu9TGG6QGv+KU5POoZaGLDkRE7TzWkyyP0KQLo80K99MssZCpgSeg==", + "dev": true, "license": "MIT", "dependencies": { "core-js": "^3.8.2", @@ -5500,6 +5545,7 @@ "version": "6.5.16", "resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-6.5.16.tgz", "integrity": "sha512-pxcNaCj3ItDdicPTXTtmYJE3YC1SjxFrBmHcyrN+nffeNyiMuViJdOOZzzzucTUG0wcOOX8jaSyak+nnHg5H1Q==", + "dev": true, "license": "MIT", "dependencies": { "core-js": "^3.8.2", @@ -5514,6 +5560,7 @@ "version": "6.5.16", "resolved": "https://registry.npmjs.org/@storybook/components/-/components-6.5.16.tgz", "integrity": "sha512-LzBOFJKITLtDcbW9jXl0/PaG+4xAz25PK8JxPZpIALbmOpYWOAPcO6V9C2heX6e6NgWFMUxjplkULEk9RCQMNA==", + "dev": true, "license": "MIT", "dependencies": { "@storybook/client-logger": "6.5.16", @@ -5538,6 +5585,7 @@ "version": "0.13.11", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "dev": true, "license": "MIT" }, "node_modules/@storybook/core": { @@ -5704,6 +5752,7 @@ "version": "6.5.16", "resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-6.5.16.tgz", "integrity": "sha512-qMZQwmvzpH5F2uwNUllTPg6eZXr2OaYZQRRN8VZJiuorZzDNdAFmiVWMWdkThwmyLEJuQKXxqCL8lMj/7PPM+g==", + "dev": true, "license": "MIT", "dependencies": { "core-js": "^3.8.2" @@ -5804,6 +5853,7 @@ "version": "0.0.2--canary.4566f4d.1", "resolved": "https://registry.npmjs.org/@storybook/csf/-/csf-0.0.2--canary.4566f4d.1.tgz", "integrity": "sha512-9OVvMVh3t9znYZwb0Svf/YQoxX2gVOeQTGe2bses2yj+a3+OJnCrUF3/hGv6Em7KujtOdL2LL+JnG49oMVGFgQ==", + "dev": true, "license": "MIT", "dependencies": { "lodash": "^4.17.15" @@ -6276,6 +6326,7 @@ "version": "6.5.16", "resolved": "https://registry.npmjs.org/@storybook/router/-/router-6.5.16.tgz", "integrity": "sha512-ZgeP8a5YV/iuKbv31V8DjPxlV4AzorRiR8OuSt/KqaiYXNXlOoQDz/qMmiNcrshrfLpmkzoq7fSo4T8lWo2UwQ==", + "dev": true, "license": "MIT", "dependencies": { "@storybook/client-logger": "6.5.16", @@ -6297,12 +6348,14 @@ "version": "0.13.11", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "dev": true, "license": "MIT" }, "node_modules/@storybook/semver": { "version": "7.3.2", "resolved": "https://registry.npmjs.org/@storybook/semver/-/semver-7.3.2.tgz", "integrity": "sha512-SWeszlsiPsMI0Ps0jVNtH64cI5c0UF3f7KgjVKJoNP30crQ6wUSddY2hsdeczZXEKVJGEn50Q60flcGsQGIcrg==", + "dev": true, "license": "ISC", "dependencies": { "core-js": "^3.6.5", @@ -6319,6 +6372,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, "license": "MIT", "dependencies": { "locate-path": "^5.0.0", @@ -6332,6 +6386,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, "license": "MIT", "dependencies": { "p-locate": "^4.1.0" @@ -6344,6 +6399,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, "license": "MIT", "dependencies": { "p-try": "^2.0.0" @@ -6359,6 +6415,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, "license": "MIT", "dependencies": { "p-limit": "^2.2.0" @@ -6442,6 +6499,7 @@ "version": "6.5.16", "resolved": "https://registry.npmjs.org/@storybook/theming/-/theming-6.5.16.tgz", "integrity": "sha512-hNLctkjaYLRdk1+xYTkC1mg4dYz2wSv6SqbLpcKMbkPHTE0ElhddGPHQqB362md/w9emYXNkt1LSMD8Xk9JzVQ==", + "dev": true, "license": "MIT", "dependencies": { "@storybook/client-logger": "6.5.16", @@ -6462,6 +6520,7 @@ "version": "0.13.11", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "dev": true, "license": "MIT" }, "node_modules/@storybook/ui": { @@ -7088,6 +7147,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/@types/is-function/-/is-function-1.0.3.tgz", "integrity": "sha512-/CLhCW79JUeLKznI6mbVieGbl4QU5Hfn+6udw1YHZoofASjbQ5zaP5LzAUZYDpRYEjS4/P+DhEgyJ/PQmGGTWw==", + "dev": true, "license": "MIT" }, "node_modules/@types/isomorphic-fetch": { @@ -7437,6 +7497,12 @@ "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", "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", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/@types/webpack": { "version": "4.41.40", "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.40.tgz", @@ -7456,6 +7522,7 @@ "version": "1.18.8", "resolved": "https://registry.npmjs.org/@types/webpack-env/-/webpack-env-1.18.8.tgz", "integrity": "sha512-G9eAoJRMLjcvN4I08wB5I7YofOb/kaJNd5uoCMX+LbKXTPCF+ZIHuqTnFaK9Jz1rgs035f9JUPUhNFtqgucy/A==", + "dev": true, "license": "MIT" }, "node_modules/@types/webpack-sources": { @@ -17528,6 +17595,16 @@ "node": ">= 4" } }, + "node_modules/immer": { + "version": "11.1.3", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.3.tgz", + "integrity": "sha512-6jQTc5z0KJFtr1UgFpIL3N9XSC3saRaI9PwWtzM2pSqkNGtiNkYY2OSwkOGDK2XcTRcLb1pi/aNkKZz0nxVH4Q==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -18027,6 +18104,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-function/-/is-function-1.0.2.tgz", "integrity": "sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==", + "dev": true, "license": "MIT" }, "node_modules/is-generator-function": { @@ -18149,6 +18227,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -18226,6 +18305,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -18697,13 +18777,6 @@ "node": ">= 10.13.0" } }, - "node_modules/jquery": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", - "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==", - "license": "MIT", - "peer": true - }, "node_modules/js-sha3": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", @@ -19343,6 +19416,7 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/map-or-similar/-/map-or-similar-1.5.0.tgz", "integrity": "sha512-0aF7ZmVon1igznGI4VS30yugpduQW3y3GkcgGJOp7d8x8QrizhigUxjI/m2UojsXXto+jLAH3KSz+xOJTiORjg==", + "dev": true, "license": "MIT" }, "node_modules/map-visit": { @@ -20535,6 +20609,7 @@ "version": "1.11.3", "resolved": "https://registry.npmjs.org/memoizerific/-/memoizerific-1.11.3.tgz", "integrity": "sha512-/EuHYwAPdLtXwAwSZkh/Gutery6pD2KYd44oQLhAvQp/50mpyduZh8Q7PYHXTCJ+wuXxt7oij2LXyIJOOYFPog==", + "dev": true, "license": "MIT", "dependencies": { "map-or-similar": "^1.5.0" @@ -20591,21 +20666,6 @@ "safe-buffer": "~5.1.0" } }, - "node_modules/meow": { - "version": "13.2.0", - "resolved": "https://registry.npmjs.org/meow/-/meow-13.2.0.tgz", - "integrity": "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/merge-descriptors": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", @@ -22516,6 +22576,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -22718,6 +22779,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -22870,18 +22932,6 @@ "node": ">=6" } }, - "node_modules/popper.js": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz", - "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==", - "deprecated": "You can find the new Popper v2 at @popperjs/core, this package is dedicated to the legacy v1", - "license": "MIT", - "peer": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/popperjs" - } - }, "node_modules/portable-fetch": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/portable-fetch/-/portable-fetch-3.0.0.tgz", @@ -23769,6 +23819,29 @@ } } }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, "node_modules/react-refresh": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", @@ -24076,6 +24149,21 @@ "node": ">=8" } }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -27176,6 +27264,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -28052,6 +28141,7 @@ "version": "2.14.4", "resolved": "https://registry.npmjs.org/store2/-/store2-2.14.4.tgz", "integrity": "sha512-srTItn1GOvyvOycgxjAnPA63FZNwy0PTyUBFMHRM+hVFltAeoh0LmNBz9SZqUS9mMqGk8rfyWyXn3GH5ReJ8Zw==", + "dev": true, "license": "MIT" }, "node_modules/stream-browserify": { @@ -28685,6 +28775,7 @@ "version": "6.0.8", "resolved": "https://registry.npmjs.org/telejson/-/telejson-6.0.8.tgz", "integrity": "sha512-nerNXi+j8NK1QEfBHtZUN/aLdDcyupA//9kAboYLrtzZlPLpUfqbVGWb9zz91f/mIjRbAYhbgtnJHY8I1b5MBg==", + "dev": true, "license": "MIT", "dependencies": { "@types/is-function": "^1.0.0", @@ -28701,6 +28792,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/isobject/-/isobject-4.0.0.tgz", "integrity": "sha512-S/2fF5wH8SJA/kmwr6HYhK/RI/OkhD84k8ntalo0iJjZikgq1XFvR5M8NPT1x5F7fBwCG3qHfnzeP/Vh/ZxCUA==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -29222,6 +29314,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.10" @@ -30114,6 +30207,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, "license": "MIT" }, "node_modules/util.promisify": { @@ -31969,21 +32063,6 @@ "dev": true, "license": "ISC" }, - "node_modules/yaml": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz", - "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==", - "dev": true, - "license": "ISC", - "optional": true, - "peer": true, - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/yargs": { "version": "16.2.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", diff --git a/hwproj.front/package.json b/hwproj.front/package.json index 0ddb05582..9698a7bd2 100644 --- a/hwproj.front/package.json +++ b/hwproj.front/package.json @@ -16,6 +16,7 @@ "@mui/lab": "^5.0.0-alpha.99", "@mui/material": "^5.16.11", "@mui/x-charts": "^8.2.0", + "@reduxjs/toolkit": "^2.11.2", "@storybook/addon-knobs": "^6.3.0", "@types/bluebird": "^3.5.36", "@types/classnames": "^2.3.1", @@ -48,6 +49,7 @@ "react-drag-drop-files": "^3.1.0", "react-markdown": "^5.0.0", "react-query": "^3.21.1", + "react-redux": "^9.2.0", "react-router-dom": "^6.5.0", "react-social-login-buttons": "^3.5.1", "react-syntax-highlighter": "^15.5.0", diff --git a/hwproj.front/src/store/index.ts b/hwproj.front/src/store/index.ts new file mode 100644 index 000000000..e69de29bb From 75ab88fdf1ac85fbb558e8116a030bb4b2c5a452 Mon Sep 17 00:00:00 2001 From: Andrew Kalinin Date: Wed, 31 Dec 2025 02:33:22 +0300 Subject: [PATCH 02/23] feat: add index.ts and hooks.ts --- hwproj.front/src/store/hooks.ts | 5 +++++ hwproj.front/src/store/index.ts | 10 ++++++++++ 2 files changed, 15 insertions(+) create mode 100644 hwproj.front/src/store/hooks.ts diff --git a/hwproj.front/src/store/hooks.ts b/hwproj.front/src/store/hooks.ts new file mode 100644 index 000000000..753d94ac6 --- /dev/null +++ b/hwproj.front/src/store/hooks.ts @@ -0,0 +1,5 @@ +import {useDispatch, useSelector} from 'react-redux'; +import type {RootState, AppDispatch} from './index'; + +export const useAppDispatch = useDispatch.withTypes(); +export const useAppSelector = useSelector.withTypes(); \ No newline at end of file diff --git a/hwproj.front/src/store/index.ts b/hwproj.front/src/store/index.ts index e69de29bb..1e6f81be3 100644 --- a/hwproj.front/src/store/index.ts +++ b/hwproj.front/src/store/index.ts @@ -0,0 +1,10 @@ +import {configureStore} from '@reduxjs/toolkit'; + +export const store = configureStore({ + reducer: { + + }, +}); + +export type RootState = ReturnType; +export type AppDispatch = typeof store.dispatch; \ No newline at end of file From fcc37725f3c0664cbccca5b6d6877f56acc4b2e5 Mon Sep 17 00:00:00 2001 From: Andrew Kalinin Date: Wed, 31 Dec 2025 07:29:53 +0300 Subject: [PATCH 03/23] feat: upd store --- hwproj.front/src/store/index.ts | 5 ++++- hwproj.front/src/store/slices/courseSlice.ts | 0 hwproj.front/src/store/slices/homeworkSlice.ts | 0 3 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 hwproj.front/src/store/slices/courseSlice.ts create mode 100644 hwproj.front/src/store/slices/homeworkSlice.ts diff --git a/hwproj.front/src/store/index.ts b/hwproj.front/src/store/index.ts index 1e6f81be3..6caf9112b 100644 --- a/hwproj.front/src/store/index.ts +++ b/hwproj.front/src/store/index.ts @@ -1,8 +1,11 @@ import {configureStore} from '@reduxjs/toolkit'; +import courseReducer from './slices/courseSlice'; +import homeworkReducer from './slices/homeworkSlice'; export const store = configureStore({ reducer: { - + course: courseReducer, + homework: homeworkReducer, }, }); diff --git a/hwproj.front/src/store/slices/courseSlice.ts b/hwproj.front/src/store/slices/courseSlice.ts new file mode 100644 index 000000000..e69de29bb diff --git a/hwproj.front/src/store/slices/homeworkSlice.ts b/hwproj.front/src/store/slices/homeworkSlice.ts new file mode 100644 index 000000000..e69de29bb From 37e0b7728963447cd8acfd77c413d461cffb8dd2 Mon Sep 17 00:00:00 2001 From: Andrew Kalinin Date: Wed, 31 Dec 2025 09:07:22 +0300 Subject: [PATCH 04/23] feat: upd store --- hwproj.front/src/store/slices/courseSlice.ts | 69 ++++++++++++++++ .../src/store/slices/homeworkSlice.ts | 81 +++++++++++++++++++ 2 files changed, 150 insertions(+) diff --git a/hwproj.front/src/store/slices/courseSlice.ts b/hwproj.front/src/store/slices/courseSlice.ts index e69de29bb..66533f841 100644 --- a/hwproj.front/src/store/slices/courseSlice.ts +++ b/hwproj.front/src/store/slices/courseSlice.ts @@ -0,0 +1,69 @@ +import {createSlice, PayloadAction} from '@reduxjs/toolkit'; +import {AccountDataDto, CourseViewModel} from '@/api' + + +interface CourseState { + isFound: boolean; + isLoading: boolean; + course: CourseViewModel | null; + mentors: AccountDataDto[]; + acceptedStudents: AccountDataDto[]; + newStudents: AccountDataDto[]; +} + +const initialState: CourseState = { + isFound: false, + isLoading: false, + course: null, + mentors: [], + acceptedStudents: [], + newStudents: [], +}; + +const courseSlice = createSlice({ + name: 'course', + initialState, + reducers: { + setCourse(state, action: PayloadAction) { + state.course = action.payload; + state.isFound = true; + state.isLoading = false; + }, + + setMentors(state, action: PayloadAction) { + state.mentors = action.payload; + }, + + setAcceptedStudents(state, action: PayloadAction) { + state.acceptedStudents = action.payload + }, + + setNewStudents(state, action: PayloadAction) { + state.newStudents = action.payload + }, + + setLoading(state, action: PayloadAction) { + state.isLoading = action.payload; + }, + + resetCourse(state) { + state.course = null; + state.isFound = false; + state.isLoading = false; + state.mentors = []; + state.acceptedStudents = []; + state.newStudents = []; + }, + }, +}); + +export const { + setCourse, + setMentors, + setAcceptedStudents, + setNewStudents, + setLoading, + resetCourse +} = courseSlice.actions; + +export default courseSlice.reducer; \ No newline at end of file diff --git a/hwproj.front/src/store/slices/homeworkSlice.ts b/hwproj.front/src/store/slices/homeworkSlice.ts index e69de29bb..171ea855b 100644 --- a/hwproj.front/src/store/slices/homeworkSlice.ts +++ b/hwproj.front/src/store/slices/homeworkSlice.ts @@ -0,0 +1,81 @@ +import {createSlice, PayloadAction } from '@reduxjs/toolkit'; +import {HomeworkViewModel, HomeworkTaskViewModel } from '@/api'; + +interface HomeworkState { + homeworks: HomeworkViewModel[]; + isLoading: boolean +} + +const initialState: HomeworkState = { + homeworks: [], + isLoading: false, +} + +const homeworkSlice = createSlice({ + name: 'homework', + initialState, + reducers: { + setHomeworks(state, action: PayloadAction) { + state.homeworks = action.payload; + state.isLoading = false; + }, + + addHomework(state, action: PayloadAction) { + state.homeworks.push(action.payload); + }, + + updateHomework(state, action: PayloadAction) { + const index = state.homeworks.findIndex(hw => hw.id === action.payload.id); + if (index !== -1) { + state.homeworks[index] = action.payload; + } + }, + + deleteHomework(state, action: PayloadAction) { + state.homeworks = state.homeworks.filter(hw => hw.id !== action.payload); + }, + + updateTask(state, action: PayloadAction) { + const task = action.payload; + const homework = state.homeworks.find(hw => hw.id === task.homeworkId); + if (homework && homework.tasks) { + const taskIndex = homework.tasks.findIndex(t => t.id === task.id); + if (taskIndex !== -1) { + homework.tasks[taskIndex] = task; + } + else { + homework.tasks.push(task); + } + } + }, + + deleteTask(state, action: PayloadAction<{homeworkId: number, taskId: number}>) { + const homework = state.homeworks.find(hw => hw.id === action.payload.homeworkId); + if (homework && homework.tasks) { + homework.tasks = homework.tasks.filter(t => t.id !== action.payload.taskId); + } + }, + + setHomeworkLoading(state, action: PayloadAction) { + state.isLoading = action.payload; + }, + + resetHomeworks(state) { + state.homeworks = []; + state.isLoading = false; + }, + }, +}); + +export const { + setHomeworks, + addHomework, + updateHomework, + deleteHomework, + updateTask, + deleteTask, + setHomeworkLoading, + resetHomeworks, +} = homeworkSlice.actions; + +export default homeworkSlice.reducer; \ No newline at end of file From 21c7785eb7aebd870edd6d4b89cd98e338ed21da Mon Sep 17 00:00:00 2001 From: Andrew Kalinin Date: Sun, 4 Jan 2026 02:16:12 +0300 Subject: [PATCH 05/23] feat: connect the Redux Provider --- hwproj.front/src/index.tsx | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/hwproj.front/src/index.tsx b/hwproj.front/src/index.tsx index f63d3e5db..3e813aefa 100644 --- a/hwproj.front/src/index.tsx +++ b/hwproj.front/src/index.tsx @@ -8,6 +8,8 @@ import {BrowserRouter} from "react-router-dom"; import ThemeProvider from "@material-ui/styles/ThemeProvider"; import {createTheme} from "@material-ui/core/styles"; import {SnackbarProvider} from "notistack"; +import { Provider } from "react-redux"; +import { store } from "./store"; const theme = createTheme({ typography: { @@ -22,13 +24,15 @@ const theme = createTheme({ }); ReactDOM.render( - - - - - - - , + + + + + + + + + , document.getElementById("root") ); From f4eb53af6aaaa6ff9f0fa0b31eb247bb4f7db9a3 Mon Sep 17 00:00:00 2001 From: Andrew Kalinin Date: Sun, 4 Jan 2026 05:19:35 +0300 Subject: [PATCH 06/23] refactor: replaced useState with useAppSelector/dispatch in Course.tsx --- .../src/components/Courses/Course.tsx | 92 ++++++------------- 1 file changed, 30 insertions(+), 62 deletions(-) diff --git a/hwproj.front/src/components/Courses/Course.tsx b/hwproj.front/src/components/Courses/Course.tsx index c2309e0fe..220c0ce13 100644 --- a/hwproj.front/src/components/Courses/Course.tsx +++ b/hwproj.front/src/components/Courses/Course.tsx @@ -40,6 +40,9 @@ import {MoreVert} from "@mui/icons-material"; import {DotLottieReact} from "@lottiefiles/dotlottie-react"; import {CourseUnitType} from "../Files/CourseUnitType"; import {FileStatus} from "../Files/FileStatus"; +import { useAppDispatch, useAppSelector } from "@/store/hooks"; +import { setCourse, setMentors, setAcceptedStudents, setNewStudents } from "@/store/slices/courseSlice"; +import { setHomeworks } from "@/store/slices/homeworkSlice"; type TabValue = "homeworks" | "stats" | "applications" @@ -47,17 +50,6 @@ function isAcceptableTabValue(str: string): str is TabValue { return str === "homeworks" || str === "stats" || str === "applications"; } -interface ICourseState { - isFound: boolean; - course: CourseViewModel; - courseHomeworks: HomeworkViewModel[]; - mentors: AccountDataDto[]; - acceptedStudents: AccountDataDto[]; - newStudents: AccountDataDto[]; - studentSolutions: StatisticsCourseMatesModel[]; - showQrCode: boolean; -} - interface ICourseFilesState { processingFilesState: { [homeworkId: number]: { @@ -78,16 +70,15 @@ const Course: React.FC = () => { const navigate = useNavigate() const {enqueueSnackbar} = useSnackbar() - const [courseState, setCourseState] = useState({ - isFound: false, - course: {}, - courseHomeworks: [], - mentors: [], - acceptedStudents: [], - newStudents: [], - studentSolutions: [], - showQrCode: false - }) + const dispatch = useAppDispatch(); + const course = useAppSelector(state => state.course.course); + const isFound = useAppSelector(state => state.course.isFound); + const mentors = useAppSelector(state => state.course.mentors); + const acceptedStudents = useAppSelector(state => state.course.acceptedStudents); + const newStudents = useAppSelector(state => state.course.newStudents); + const courseHomeworks = useAppSelector(state => state.homework.homeworks); + const [showQrCode, setShowQrCode] = useState(false); + const [studentSolutions, setStudentSolutions] = useState(undefined) const [courseFilesState, setCourseFilesState] = useState({ processingFilesState: {}, @@ -223,15 +214,6 @@ const Course: React.FC = () => { tabValue: "homeworks" }) - const { - isFound, - course, - mentors, - newStudents, - acceptedStudents, - courseHomeworks, - } = courseState - const userId = ApiSingleton.authService.getUserId() const isLecturer = ApiSingleton.authService.isLecturer() @@ -272,16 +254,11 @@ const Course: React.FC = () => { return } - setCourseState(prevState => ({ - ...prevState, - isFound: true, - course: course, - courseHomeworks: course.homeworks!, - createHomework: false, - mentors: course.mentors!, - acceptedStudents: course.acceptedStudents!, - newStudents: course.newStudents!, - })) + dispatch(setCourse(course)); + dispatch(setMentors(course.mentors!)); + dispatch(setAcceptedStudents(course.acceptedStudents!)); + dispatch(setNewStudents(course.newStudents!)); + dispatch(setHomeworks(course.homeworks!)); } const getCourseFilesInfo = async () => { @@ -367,10 +344,7 @@ const Course: React.FC = () => { Управление } - setCourseState(prevState => ({ - ...prevState, - showQrCode: true - }))}> + setShowQrCode(true)}> @@ -391,8 +365,8 @@ const Course: React.FC = () => { return (
setCourseState(prevState => ({...prevState, showQrCode: false}))} + open={showQrCode} + onClose={() => setShowQrCode(false)} > Поделитесь ссылкой на курс с помощью QR-кода @@ -407,7 +381,7 @@ const Course: React.FC = () => { - {course.isCompleted && + {course?.isCompleted && Курс завершен! {isAcceptedStudent @@ -422,7 +396,7 @@ const Course: React.FC = () => { - {NameBuilder.getCourseFullName(course.name!, course.groupName)} + {NameBuilder.getCourseFullName(course?.name || "", course?.groupName || "")} @@ -499,21 +473,18 @@ const Course: React.FC = () => { processingFiles={courseFilesState.processingFilesState} onStartProcessing={getFilesByInterval} onHomeworkUpdate={({fileInfos, homework, isDeleted}) => { - const homeworkIndex = courseState.courseHomeworks.findIndex(x => x.id === homework.id) - const homeworks = courseState.courseHomeworks + const homeworkIndex = courseHomeworks.findIndex(x => x.id === homework.id) + const homeworks = [...courseHomeworks] if (isDeleted) homeworks.splice(homeworkIndex, 1) else if (homeworkIndex === -1) homeworks.push(homework) else homeworks[homeworkIndex] = homework - setCourseState(prevState => ({ - ...prevState, - courseHomeworks: homeworks - })) + dispatch(setHomeworks(homeworks)); }} onTaskUpdate={update => { const task = update.task - const homeworks = courseState.courseHomeworks + const homeworks = [...courseHomeworks] const homework = homeworks.find(x => x.id === task.homeworkId)! const tasks = [...homework.tasks!] const taskIndex = tasks.findIndex(x => x!.id === task.id) @@ -524,10 +495,7 @@ const Course: React.FC = () => { homework.tasks = tasks - setCourseState(prevState => ({ - ...prevState, - courseHomeworks: homeworks - })) + dispatch(setHomeworks(homeworks)); }} /> } @@ -538,7 +506,7 @@ const Course: React.FC = () => { homeworks={courseHomeworks} userId={userId as string} isMentor={isCourseMentor} - course={courseState.course} + course={course!} solutions={studentSolutions} /> @@ -546,8 +514,8 @@ const Course: React.FC = () => { {tabValue === "applications" && showApplicationsTab && setCurrentState()} - course={courseState.course} - students={courseState.newStudents} + course={course!} + students={newStudents} courseId={courseId!} /> } From 8b6225a75f1530e296d8f3601c18db3333c95af7 Mon Sep 17 00:00:00 2001 From: Andrew Kalinin Date: Sun, 4 Jan 2026 06:30:36 +0300 Subject: [PATCH 07/23] feat: integrate Redux for Course/CourseExperimental --- .../src/components/Courses/Course.tsx | 26 ---------- .../components/Courses/CourseExperimental.tsx | 51 +++++++++++++++---- 2 files changed, 41 insertions(+), 36 deletions(-) diff --git a/hwproj.front/src/components/Courses/Course.tsx b/hwproj.front/src/components/Courses/Course.tsx index 220c0ce13..168b93c2a 100644 --- a/hwproj.front/src/components/Courses/Course.tsx +++ b/hwproj.front/src/components/Courses/Course.tsx @@ -463,7 +463,6 @@ const Course: React.FC = () => { {tabValue === "homeworks" && { userId={userId!} processingFiles={courseFilesState.processingFilesState} onStartProcessing={getFilesByInterval} - onHomeworkUpdate={({fileInfos, homework, isDeleted}) => { - const homeworkIndex = courseHomeworks.findIndex(x => x.id === homework.id) - const homeworks = [...courseHomeworks] - - if (isDeleted) homeworks.splice(homeworkIndex, 1) - else if (homeworkIndex === -1) homeworks.push(homework) - else homeworks[homeworkIndex] = homework - - dispatch(setHomeworks(homeworks)); - }} - onTaskUpdate={update => { - const task = update.task - const homeworks = [...courseHomeworks] - const homework = homeworks.find(x => x.id === task.homeworkId)! - const tasks = [...homework.tasks!] - const taskIndex = tasks.findIndex(x => x!.id === task.id) - - if (update.isDeleted) tasks.splice(taskIndex, 1) - else if (taskIndex !== -1) tasks![taskIndex] = task - else tasks.push(task) - - homework.tasks = tasks - - dispatch(setHomeworks(homeworks)); - }} /> } {tabValue === "stats" && diff --git a/hwproj.front/src/components/Courses/CourseExperimental.tsx b/hwproj.front/src/components/Courses/CourseExperimental.tsx index b5d1819fe..7b3a011fc 100644 --- a/hwproj.front/src/components/Courses/CourseExperimental.tsx +++ b/hwproj.front/src/components/Courses/CourseExperimental.tsx @@ -4,6 +4,8 @@ import { HomeworkTaskViewModel, HomeworkViewModel, Solution, StatisticsCourseMatesModel, } from "@/api"; +import { useAppDispatch, useAppSelector } from "@/store/hooks"; +import {setHomeworks} from "@/store/slices/homeworkSlice"; import { AlertTitle, Button, @@ -37,7 +39,6 @@ import SwitchAccessShortcutIcon from '@mui/icons-material/SwitchAccessShortcut'; import Lodash from "lodash"; interface ICourseExperimentalProps { - homeworks: HomeworkViewModel[] courseFilesInfo: FileInfoDTO[] studentSolutions: StatisticsCourseMatesModel[] courseId: number @@ -45,10 +46,6 @@ interface ICourseExperimentalProps { isStudentAccepted: boolean userId: string selectedHomeworkId: number | undefined - onHomeworkUpdate: (update: { homework: HomeworkViewModel, fileInfos: FileInfoDTO[] | undefined } & { - isDeleted?: boolean - }) => void - onTaskUpdate: (update: { task: HomeworkTaskViewModel, isDeleted?: boolean }) => void, processingFiles: { [homeworkId: number]: { isLoading: boolean; @@ -66,6 +63,8 @@ interface ICourseExperimentalState { } export const CourseExperimental: FC = (props) => { + const dispatch = useAppDispatch() + const allHomeworks = useAppSelector(state => state.homework.homeworks) const [hideDeferred, setHideDeferred] = useState(false) const [showOnlyGroupedTest, setShowOnlyGroupedTest] = useState(undefined) const filterAdded = hideDeferred || showOnlyGroupedTest !== undefined @@ -77,7 +76,7 @@ export const CourseExperimental: FC = (props) => { // Состояние для кнопки "Наверх" const [showScrollButton, setShowScrollButton] = useState(false); - const homeworks = props.homeworks.slice().reverse().filter(x => { + const homeworks = allHomeworks.slice().reverse().filter(x => { if (hideDeferred) return !x.isDeferred if (showOnlyGroupedTest !== undefined) return x.tags!.includes(TestTag) && x.tags!.includes(showOnlyGroupedTest) return true @@ -340,8 +339,40 @@ export const CourseExperimental: FC = (props) => { const [newTaskCounter, setNewTaskCounter] = useState(-1) + const handleHomeworkUpdate = (update: { homework: HomeworkViewModel, fileInfos?: FileInfoDTO[], isDeleted?: boolean }) => { + const { homework, isDeleted } = update; + const homeworkIndex = allHomeworks.findIndex(h => h.id === homework.id); + const newHomeworks = [...allHomeworks]; + + if (isDeleted) newHomeworks.splice(homeworkIndex, 1); + else if (homeworkIndex === -1) newHomeworks.push(homework); + else newHomeworks[homeworkIndex] = homework; + + dispatch(setHomeworks(newHomeworks)) + }; + + const handleTaskUpdate = (update: { task: HomeworkTaskViewModel, isDeleted?: boolean }) => { + const { task, isDeleted } = update; + const homeworkIndex = allHomeworks.findIndex(h => h.id === task.homeworkId); + + if (homeworkIndex === -1) return; + + const homework = allHomeworks[homeworkIndex]; + const tasks = [...(homework.tasks || [])]; + const taskIndex = tasks.findIndex(t => t?.id === task.id); + + if (isDeleted) tasks.splice(taskIndex, 1); + else if (taskIndex === -1) tasks.push(task); + else tasks[taskIndex] = task; + + const updatedHomework = { ...homework, tasks }; + const newHomeworks = [...allHomeworks]; + newHomeworks[homeworkIndex] = updatedHomework; + dispatch(setHomeworks(newHomeworks)) + } + const addNewHomework = () => { - props.onHomeworkUpdate({ + handleHomeworkUpdate({ homework: { courseId: props.courseId, title: "Новое задание", @@ -398,7 +429,7 @@ export const CourseExperimental: FC = (props) => { id } - props.onTaskUpdate({task}) + handleTaskUpdate({task}) setState((prevState) => ({ ...prevState, selectedItem: { @@ -425,7 +456,7 @@ export const CourseExperimental: FC = (props) => { onMount={onSelectedItemMount} onAddTask={addNewTask} onUpdate={update => { - props.onHomeworkUpdate(update) + handleHomeworkUpdate(update) setState((prevState) => ({ ...prevState, selectedItem: { @@ -454,7 +485,7 @@ export const CourseExperimental: FC = (props) => { initialEditMode={initialEditMode || taskEditMode} onMount={onSelectedItemMount} onUpdate={update => { - props.onTaskUpdate(update) + handleTaskUpdate(update) if (update.isDeleted) setState((prevState) => ({ ...prevState, From e8154586d687c0e58418589674fea507724f7fbb Mon Sep 17 00:00:00 2001 From: Andrew Kalinin Date: Sun, 4 Jan 2026 14:52:14 +0300 Subject: [PATCH 08/23] feat: add courseFileSlice.ts and solutionSlice.ts with update index.ts --- hwproj.front/src/store/index.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/hwproj.front/src/store/index.ts b/hwproj.front/src/store/index.ts index 6caf9112b..1ee9758cf 100644 --- a/hwproj.front/src/store/index.ts +++ b/hwproj.front/src/store/index.ts @@ -1,11 +1,15 @@ import {configureStore} from '@reduxjs/toolkit'; import courseReducer from './slices/courseSlice'; import homeworkReducer from './slices/homeworkSlice'; +import solutionsReducer from './slices/solutionSlice'; +import courseFilesReducer from './slices/courseFileSlice'; export const store = configureStore({ reducer: { course: courseReducer, homework: homeworkReducer, + solutions: solutionsReducer, + courseFiles: courseFilesReducer, }, }); From 8e6bc5403df28b4562ea9141c412b37f305d198e Mon Sep 17 00:00:00 2001 From: Andrew Kalinin Date: Sun, 4 Jan 2026 15:01:54 +0300 Subject: [PATCH 09/23] feat: solutionSlice and courseFileSlice integrate into Course.tsx --- .../src/components/Courses/Course.tsx | 73 +++++-------------- 1 file changed, 20 insertions(+), 53 deletions(-) diff --git a/hwproj.front/src/components/Courses/Course.tsx b/hwproj.front/src/components/Courses/Course.tsx index 168b93c2a..338abbe84 100644 --- a/hwproj.front/src/components/Courses/Course.tsx +++ b/hwproj.front/src/components/Courses/Course.tsx @@ -43,6 +43,8 @@ import {FileStatus} from "../Files/FileStatus"; import { useAppDispatch, useAppSelector } from "@/store/hooks"; import { setCourse, setMentors, setAcceptedStudents, setNewStudents } from "@/store/slices/courseSlice"; import { setHomeworks } from "@/store/slices/homeworkSlice"; +import { setStudentSolutions } from "@/store/slices/solutionSlice"; +import { setCourseFiles, updateCourseFiles, setProcessingLoading } from "@/store/slices/courseFileSlice"; type TabValue = "homeworks" | "stats" | "applications" @@ -50,16 +52,6 @@ function isAcceptableTabValue(str: string): str is TabValue { return str === "homeworks" || str === "stats" || str === "applications"; } -interface ICourseFilesState { - processingFilesState: { - [homeworkId: number]: { - isLoading: boolean; - intervalId?: NodeJS.Timeout; - }; - }; - courseFiles: FileInfoDTO[]; -} - interface IPageState { tabValue: TabValue } @@ -77,45 +69,23 @@ const Course: React.FC = () => { const acceptedStudents = useAppSelector(state => state.course.acceptedStudents); const newStudents = useAppSelector(state => state.course.newStudents); const courseHomeworks = useAppSelector(state => state.homework.homeworks); + const studentSolutions = useAppSelector(state => state.solutions.studentSolutions); + const courseFiles = useAppSelector(state => state.courseFiles.courseFiles); + const processingFilesState = useAppSelector(state => state.courseFiles.processingFilesState); const [showQrCode, setShowQrCode] = useState(false); - const [studentSolutions, setStudentSolutions] = useState(undefined) - const [courseFilesState, setCourseFilesState] = useState({ - processingFilesState: {}, - courseFiles: [] - }) - const intervalsRef = React.useRef>({}); - const updateCourseFiles = (files: FileInfoDTO[], unitType: CourseUnitType, unitId: number) => { - setCourseFilesState(prev => ({ - ...prev, - courseFiles: [ - ...prev.courseFiles.filter( - f => !(f.courseUnitType === unitType && f.courseUnitId === unitId)), - ...files - ] - })); + const handleUpdateCourseFiles = (files: FileInfoDTO[], unitType: CourseUnitType, unitId: number) => { + dispatch(updateCourseFiles({ files, unitType, unitId })); }; const setCommonLoading = (homeworkId: number) => { - setCourseFilesState(prev => ({ - ...prev, - processingFilesState: { - ...prev.processingFilesState, - [homeworkId]: {isLoading: true} - } - })); + dispatch(setProcessingLoading({ homeworkId, isLoading: true })); } const unsetCommonLoading = (homeworkId: number) => { - setCourseFilesState(prev => ({ - ...prev, - processingFilesState: { - ...prev.processingFilesState, - [homeworkId]: {isLoading: false} - } - })); + dispatch(setProcessingLoading({ homeworkId, isLoading: false })); } const stopProcessing = (homeworkId: number) => { @@ -160,14 +130,14 @@ const Course: React.FC = () => { // Первый вариант для явного отображения всех файлов if (waitingNewFilesCount === 0 && files.filter(f => f.status === FileStatus.ReadyToUse).length === previouslyExistingFilesCount - deletingFilesIds.length) { - updateCourseFiles(files, scopeDto.courseUnitType as CourseUnitType, scopeDto.courseUnitId!) + handleUpdateCourseFiles(files, scopeDto.courseUnitType as CourseUnitType, scopeDto.courseUnitId!) unsetCommonLoading(homeworkId) } // Второй вариант для явного отображения всех файлов if (waitingNewFilesCount > 0 && files.filter(f => !deletingFilesIds.some(dfi => dfi === f.id)).length === previouslyExistingFilesCount - deletingFilesIds.length + waitingNewFilesCount) { - updateCourseFiles(files, scopeDto.courseUnitType as CourseUnitType, scopeDto.courseUnitId!) + handleUpdateCourseFiles(files, scopeDto.courseUnitType as CourseUnitType, scopeDto.courseUnitId!) unsetCommonLoading(homeworkId) } @@ -266,15 +236,12 @@ const Course: React.FC = () => { try { courseFilesInfo = isCourseMentor ? await ApiSingleton.filesApi.filesGetFilesInfo(+courseId!) - : await ApiSingleton.filesApi.filesGetUploadedFilesInfo(+courseId!) + : await ApiSingleton.filesApi.filesGetUploadedFilesInfo(+courseId!); } catch (e) { const responseErrors = await ErrorsHandler.getErrorMessages(e as Response) enqueueSnackbar(responseErrors[0], {variant: "warning", autoHideDuration: 1990}); } - setCourseFilesState(prevState => ({ - ...prevState, - courseFiles: courseFilesInfo - })) + dispatch(setCourseFiles(courseFilesInfo)); } useEffect(() => { @@ -287,7 +254,7 @@ const Course: React.FC = () => { useEffect(() => { ApiSingleton.statisticsApi.statisticsGetCourseStatistics(+courseId!) - .then(res => setStudentSolutions(res)) + .then(res => dispatch(setStudentSolutions(res))) }, [courseId]) useEffect(() => changeTab(tab || "homeworks"), [tab, courseId, isFound]) @@ -300,7 +267,7 @@ const Course: React.FC = () => { const {tabValue} = pageState const searchedHomeworkId = searchParams.get("homeworkId") - const unratedSolutionsCount = (studentSolutions || []) + const unratedSolutionsCount = studentSolutions .flatMap(x => x.homeworks) .flatMap(x => x!.tasks) .filter(t => t!.solution!.slice(-1)[0]?.state === 0) //last solution @@ -463,13 +430,13 @@ const Course: React.FC = () => { {tabValue === "homeworks" && } @@ -481,7 +448,7 @@ const Course: React.FC = () => { userId={userId as string} isMentor={isCourseMentor} course={course!} - solutions={studentSolutions} + solutions={studentSolutions.length > 0 ? studentSolutions : undefined} /> } @@ -506,4 +473,4 @@ const Course: React.FC = () => {
} -export default Course +export default Course \ No newline at end of file From c895c10aad60a6196d07f91267ea9800b7fb3d60 Mon Sep 17 00:00:00 2001 From: Andrew Kalinin Date: Sun, 4 Jan 2026 17:49:37 +0300 Subject: [PATCH 10/23] feat: upd slices in store --- hwproj.front/src/store/index.ts | 2 + hwproj.front/src/store/slices/authSlice.ts | 34 ++++++++++++ .../src/store/slices/courseFileSlice.ts | 52 +++++++++++++++++++ .../src/store/slices/solutionSlice.ts | 31 +++++++++++ 4 files changed, 119 insertions(+) create mode 100644 hwproj.front/src/store/slices/authSlice.ts create mode 100644 hwproj.front/src/store/slices/courseFileSlice.ts create mode 100644 hwproj.front/src/store/slices/solutionSlice.ts diff --git a/hwproj.front/src/store/index.ts b/hwproj.front/src/store/index.ts index 1ee9758cf..9a70c695c 100644 --- a/hwproj.front/src/store/index.ts +++ b/hwproj.front/src/store/index.ts @@ -3,6 +3,7 @@ import courseReducer from './slices/courseSlice'; import homeworkReducer from './slices/homeworkSlice'; import solutionsReducer from './slices/solutionSlice'; import courseFilesReducer from './slices/courseFileSlice'; +import authReducer from './slices/authSlice'; export const store = configureStore({ reducer: { @@ -10,6 +11,7 @@ export const store = configureStore({ homework: homeworkReducer, solutions: solutionsReducer, courseFiles: courseFilesReducer, + auth: authReducer, }, }); diff --git a/hwproj.front/src/store/slices/authSlice.ts b/hwproj.front/src/store/slices/authSlice.ts new file mode 100644 index 000000000..448dba5d7 --- /dev/null +++ b/hwproj.front/src/store/slices/authSlice.ts @@ -0,0 +1,34 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; + +interface AuthState { + userId: string | null; + isLecturer: boolean; + isExpert: boolean; +} + +const initialState: AuthState = { + userId: null, + isLecturer: false, + isExpert: false, +}; + +const authSlice = createSlice({ + name: 'auth', + initialState, + reducers: { + setAuth: (state, action: PayloadAction) => { + state.userId = action.payload.userId; + state.isLecturer = action.payload.isLecturer; + state.isExpert = action.payload.isExpert; + }, + + clearAuth: (state) => { + state.userId = null; + state.isLecturer = false; + state.isExpert = false; + }, + }, +}); + +export const { setAuth, clearAuth } = authSlice.actions; +export default authSlice.reducer; \ No newline at end of file diff --git a/hwproj.front/src/store/slices/courseFileSlice.ts b/hwproj.front/src/store/slices/courseFileSlice.ts new file mode 100644 index 000000000..c12e54fd0 --- /dev/null +++ b/hwproj.front/src/store/slices/courseFileSlice.ts @@ -0,0 +1,52 @@ +import { createSlice, PayloadAction } from "@reduxjs/toolkit"; +import { FileInfoDTO } from "@/api"; +import { CourseUnitType } from "@/components/Files/CourseUnitType"; + +interface ProcessingState { + isLoading: boolean; +} + +interface CourseFilesState { + courseFiles: FileInfoDTO[]; + processingFilesState: Record; +} + +const initialState: CourseFilesState = { + courseFiles: [], + processingFilesState: {}, +} + +const courseFilesSlice = createSlice({ + name: "courseFiles", + initialState, + reducers: { + setCourseFiles(state, action: PayloadAction) { + state.courseFiles = action.payload; + }, + + updateCourseFiles(state, action: PayloadAction<{ + files: FileInfoDTO[]; + unitType: CourseUnitType; + unitId: number; + }>) { + const { files, unitType, unitId } = action.payload; + state.courseFiles = [ + ...state.courseFiles.filter(f => !(f.courseUnitType === unitType && f.courseUnitId === unitId)), + ...files + ]; + }, + + setProcessingLoading(state, action: PayloadAction<{ homeworkId: number; isLoading: boolean }>) { + const { homeworkId, isLoading } = action.payload; + state.processingFilesState[homeworkId] = { isLoading }; + }, + + clearCourseFiles(state) { + state.courseFiles = []; + state.processingFilesState = {}; + }, + }, +}) + +export const { setCourseFiles, updateCourseFiles, setProcessingLoading, clearCourseFiles } = courseFilesSlice.actions; +export default courseFilesSlice.reducer; \ No newline at end of file diff --git a/hwproj.front/src/store/slices/solutionSlice.ts b/hwproj.front/src/store/slices/solutionSlice.ts new file mode 100644 index 000000000..8c95ba730 --- /dev/null +++ b/hwproj.front/src/store/slices/solutionSlice.ts @@ -0,0 +1,31 @@ +import { createSlice, PayloadAction } from "@reduxjs/toolkit"; +import { StatisticsCourseMatesModel } from '@/api'; + +interface SolutionState { + studentSolutions: StatisticsCourseMatesModel[]; + isLoaded: boolean; +} + +const initialState: SolutionState = { + studentSolutions: [], + isLoaded: false, +}; + +const solutionSlice = createSlice({ + name: "solution", + initialState, + reducers: { + setStudentSolutions(state, action: PayloadAction) { + state.studentSolutions = action.payload; + state.isLoaded = true; + }, + + clearStudentSolutions(state) { + state.studentSolutions = []; + state.isLoaded = false; + }, + }, +}); + +export const { setStudentSolutions, clearStudentSolutions } = solutionSlice.actions; +export default solutionSlice.reducer; \ No newline at end of file From 940ddc8474403daee3bbaa545de83c07f502d8af Mon Sep 17 00:00:00 2001 From: Andrew Kalinin Date: Sun, 4 Jan 2026 17:51:47 +0300 Subject: [PATCH 11/23] refactor: add-on for Redux integration --- .../src/components/Courses/Course.tsx | 21 +++++----- .../components/Courses/CourseExperimental.tsx | 42 ++++++++++--------- 2 files changed, 32 insertions(+), 31 deletions(-) diff --git a/hwproj.front/src/components/Courses/Course.tsx b/hwproj.front/src/components/Courses/Course.tsx index 338abbe84..def40d355 100644 --- a/hwproj.front/src/components/Courses/Course.tsx +++ b/hwproj.front/src/components/Courses/Course.tsx @@ -45,6 +45,7 @@ import { setCourse, setMentors, setAcceptedStudents, setNewStudents } from "@/st import { setHomeworks } from "@/store/slices/homeworkSlice"; import { setStudentSolutions } from "@/store/slices/solutionSlice"; import { setCourseFiles, updateCourseFiles, setProcessingLoading } from "@/store/slices/courseFileSlice"; +import { setAuth } from "@/store/slices/authSlice"; type TabValue = "homeworks" | "stats" | "applications" @@ -184,10 +185,16 @@ const Course: React.FC = () => { tabValue: "homeworks" }) - const userId = ApiSingleton.authService.getUserId() + useEffect(() => { + const userId = ApiSingleton.authService.getUserId(); + const isLecturer = ApiSingleton.authService.isLecturer(); + const isExpert = ApiSingleton.authService.isExpert(); + dispatch(setAuth({ userId, isLecturer, isExpert })) + }, []) - const isLecturer = ApiSingleton.authService.isLecturer() - const isExpert = ApiSingleton.authService.isExpert() + const userId = useAppSelector(state => state.auth.userId); + const isLecturer = useAppSelector(state => state.auth.isLecturer); + const isExpert = useAppSelector(state => state.auth.isExpert); const isMentor = isLecturer || isExpert const isCourseMentor = mentors.some(t => t.userId === userId) const isSignedInCourse = newStudents!.some(cm => cm.userId === userId) @@ -429,14 +436,6 @@ const Course: React.FC = () => { }/>} {tabValue === "homeworks" && } diff --git a/hwproj.front/src/components/Courses/CourseExperimental.tsx b/hwproj.front/src/components/Courses/CourseExperimental.tsx index 7b3a011fc..ceac68bbd 100644 --- a/hwproj.front/src/components/Courses/CourseExperimental.tsx +++ b/hwproj.front/src/components/Courses/CourseExperimental.tsx @@ -25,7 +25,7 @@ import TimelineContent from '@mui/lab/TimelineContent'; import TimelineDot from '@mui/lab/TimelineDot'; import TimelineOppositeContent from '@mui/lab/TimelineOppositeContent'; import {Alert, Card, CardActions, Chip, Paper, Stack, Tooltip} from "@mui/material"; -import {Link} from "react-router-dom"; +import {Link, useSearchParams} from "react-router-dom"; import StudentStatsUtils from "../../services/StudentStatsUtils"; import {BonusTag, DefaultTags, getTip, isBonusWork, isTestWork, TestTag} from "../Common/HomeworkTags"; import FileInfoConverter from "components/Utils/FileInfoConverter"; @@ -37,20 +37,9 @@ import ErrorIcon from '@mui/icons-material/Error'; import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward'; import SwitchAccessShortcutIcon from '@mui/icons-material/SwitchAccessShortcut'; import Lodash from "lodash"; +import MentorsList from "../Common/MentorsList"; interface ICourseExperimentalProps { - courseFilesInfo: FileInfoDTO[] - studentSolutions: StatisticsCourseMatesModel[] - courseId: number - isMentor: boolean - isStudentAccepted: boolean - userId: string - selectedHomeworkId: number | undefined - processingFiles: { - [homeworkId: number]: { - isLoading: boolean; - }; - }; onStartProcessing: (homeworkId: number, previouslyExistingFilesCount: number, waitingNewFilesCount: number, deletingFilesIds: number[]) => void; } @@ -65,10 +54,22 @@ interface ICourseExperimentalState { export const CourseExperimental: FC = (props) => { const dispatch = useAppDispatch() const allHomeworks = useAppSelector(state => state.homework.homeworks) + const studentSolutions = useAppSelector(state => state.solutions.studentSolutions) + const courseFilesInfo = useAppSelector(state => state.courseFiles.courseFiles) + const processingFiles = useAppSelector(state => state.courseFiles.processingFilesState) + const mentors = useAppSelector(state => state.course.mentors) + const course = useAppSelector(state => state.course.course) + const acceptedStudents = useAppSelector(state => state.course.acceptedStudents) + const userId = useAppSelector(state => state.auth.userId) + + const courseId = course?.id ?? 0 + const isAcceptedStudent = acceptedStudents.some(s => s.userId === userId) + const [hideDeferred, setHideDeferred] = useState(false) const [showOnlyGroupedTest, setShowOnlyGroupedTest] = useState(undefined) const filterAdded = hideDeferred || showOnlyGroupedTest !== undefined + const isMentor = mentors.some(m => m.userId === userId) // Определяем разрешение экрана пользователя const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down('md')); @@ -82,7 +83,8 @@ export const CourseExperimental: FC = (props) => { return true }) - const {isMentor, studentSolutions, isStudentAccepted, userId, selectedHomeworkId, courseFilesInfo} = props + const [ searchParams ]= useSearchParams(); + const selectedHomeworkId = searchParams.get("homeworkId") ? +searchParams.get("homeworkId")! : undefined const [state, setState] = useState({ initialEditMode: false, @@ -161,7 +163,7 @@ export const CourseExperimental: FC = (props) => { const taskSolutionsMap = new Map() - if (!isMentor && isStudentAccepted) { + if (!isMentor && isAcceptedStudent) { studentSolutions .filter(t => t.id === userId) .flatMap(t => t.homeworks!) @@ -374,7 +376,7 @@ export const CourseExperimental: FC = (props) => { const addNewHomework = () => { handleHomeworkUpdate({ homework: { - courseId: props.courseId, + courseId: courseId, title: "Новое задание", publicationDateNotSet: false, publicationDate: undefined, @@ -465,7 +467,7 @@ export const CourseExperimental: FC = (props) => { } })) }} - isProcessing={props.processingFiles[homework.id!]?.isLoading || false} + isProcessing={processingFiles[homework.id!]?.isLoading || false} onStartProcessing={(homeworkId: number, previouslyExistingFilesCount: number, waitingNewFilesCount: number, deletingFilesIds: number[]) => props.onStartProcessing(homeworkId, previouslyExistingFilesCount, waitingNewFilesCount, deletingFilesIds)} /> @@ -496,7 +498,7 @@ export const CourseExperimental: FC = (props) => { })) }} toEditHomework={() => toEditHomework(homework!)} getAllHomeworks={() => homeworks}/> - {!props.isMentor && props.isStudentAccepted && < CardActions> + {!isMentor && isAcceptedStudent && < CardActions> @@ -547,7 +549,7 @@ export const CourseExperimental: FC = (props) => { borderRadius: 10 } }}> - {props.isMentor && filterAdded && + {isMentor && filterAdded &&