diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0f5ba90b0..853909327 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -2,7 +2,7 @@ name: Test, Lint, Build and Deploy on Github Pages on: push: - branches: ["master", "main"] + branches: ['master', 'main'] # Позволяет запустить этот рабочий процесс вручную на вкладке Actions workflow_dispatch: @@ -45,17 +45,17 @@ jobs: branch: gh-pages folder: dist - # # Собираем Storybook - # - name: Build Storybook - # run: npm run build-storybook - # - # # Публикуем Storybook на Github Pages - # - name: Deploy Storybook to Github Pages - # uses: JamesIves/github-pages-deploy-action@4.2.1 - # with: - # branch: gh-pages - # folder: storybook-static - # commit-message: "Automatically publish Storybook" + # Собираем Storybook + # - name: Build Storybook + # run: npm run build-storybook + + # # Публикуем Storybook на Github Pages + # - name: Deploy Storybook to Github Pages + # uses: JamesIves/github-pages-deploy-action@4.2.1 + # with: + # branch: gh-pages + # folder: storybook-static + # commit-message: 'Automatically publish Storybook' # Останавливаем выполнение строго при неудачных тестах - name: Fail on failed tests diff --git a/.storybook/main.ts b/.storybook/main.ts index 3d1c9b2d5..953c50705 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -1,18 +1,20 @@ const config = { - stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"], + stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'], + // staticDirs: ['../src/assets'], + staticDirs: [{ from: '../src/assets', to: '../../assets' }], addons: [ - "@storybook/addon-links", - "@storybook/addon-essentials", - "@storybook/addon-interactions", - "@storybook/preset-scss", - "@storybook/addon-mdx-gfm" + '@storybook/addon-links', + '@storybook/addon-essentials', + '@storybook/addon-interactions', + '@storybook/preset-scss', + '@storybook/addon-mdx-gfm', ], framework: { - name: "@storybook/react-webpack5", + name: '@storybook/react-webpack5', options: {}, }, docs: { - autodocs: "tag", + autodocs: 'tag', }, }; export default config; diff --git a/.storybook/preview-body.html b/.storybook/preview-body.html new file mode 100644 index 000000000..8b3c0a972 --- /dev/null +++ b/.storybook/preview-body.html @@ -0,0 +1 @@ +
\ No newline at end of file diff --git a/.storybook/preview.ts b/.storybook/preview.ts deleted file mode 100644 index 1c372b694..000000000 --- a/.storybook/preview.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { Preview } from "@storybook/react"; - -const preview: Preview = { - parameters: { - actions: { argTypesRegex: "^on[A-Z].*" }, - controls: { - matchers: { - color: /(background|color)$/i, - date: /Date$/, - }, - }, - }, -}; - -export default preview; diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx new file mode 100644 index 000000000..cfc60e8fc --- /dev/null +++ b/.storybook/preview.tsx @@ -0,0 +1,57 @@ +import type { Preview } from '@storybook/react'; +import 'bootstrap/dist/css/bootstrap.min.css'; +import i18n from '../src/localization/settings'; +import React, { Suspense } from 'react'; +import { I18nextProvider } from 'react-i18next'; + +const preview: Preview = { + parameters: { + actions: { argTypesRegex: '^on[A-Z].*' }, + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/, + }, + }, + }, +}; + +export default preview; + + +// Wrap your stories in the I18nextProvider component +const withI18next = (Story) => { + return ( + // This catches the suspense from components not yet ready (still loading translations) + // Alternative: set useSuspense to false on i18next.options.react when initializing i18next + loading translations...}> + + + + + ); +}; + +// export decorators for storybook to wrap your stories in +export const decorators = [withI18next]; + + +/* Snipped for brevity */ + +// Create a global variable called locale in storybook +// and add a menu in the toolbar to change your locale +export const globalTypes = { + locale: { + name: 'Locale', + description: 'Internationalization locale', + toolbar: { + icon: 'globe', + items: [ + { value: 'en', title: 'English' }, + { value: 'de', title: 'Deutsch' }, + ], + showName: true, + }, + }, +}; + diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000..ff984fe69 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,16 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "chrome", + "request": "launch", + "name": "Launch Chrome against localhost", + "url": "http://localhost:8080", + "webRoot": "${workspaceFolder}", + // "runtimeExecutable": "/Applications/Сhrome.app", + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..e8df7cb4c --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "workbench.externalBrowser": "chrome" +} \ No newline at end of file diff --git a/declarations.d.ts b/declarations.d.ts index 8ce36c1e3..414826125 100644 --- a/declarations.d.ts +++ b/declarations.d.ts @@ -1,3 +1,4 @@ +/// declare module '*.sass'; declare module '*.scss'; declare module '*.css'; @@ -5,3 +6,4 @@ declare module '*.jpg'; declare module '*.jpeg'; declare module '*.png'; declare module '*.svg'; +declare module '*d.ts'; diff --git a/package-lock.json b/package-lock.json index 5b1dfa392..e3819ae59 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,15 +9,30 @@ "version": "1.0.0", "license": "MIT", "dependencies": { + "@fortawesome/fontawesome-svg-core": "^6.6.0", + "@fortawesome/free-brands-svg-icons": "^6.6.0", + "@fortawesome/free-regular-svg-icons": "^6.6.0", + "@fortawesome/free-solid-svg-icons": "^6.6.0", + "@fortawesome/react-fontawesome": "^0.2.2", "clsx": "^1.2.1", + "i18next": "^23.15.2", + "i18next-browser-languagedetector": "^8.0.0", + "i18next-http-backend": "^2.6.2", + "intersection-observer": "^0.12.2", + "module-alias": "^2.2.3", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "react-hook-form": "^7.53.2", + "react-i18next": "^15.0.2", + "react-intersection-observer": "^9.13.1" }, "devDependencies": { "@babel/core": "^7.22.1", "@babel/preset-env": "^7.22.4", "@babel/preset-react": "^7.22.3", "@babel/preset-typescript": "^7.21.5", + "@faker-js/faker": "^9.0.2", + "@reduxjs/toolkit": "^2.4.0", "@storybook/addon-essentials": "^7.6.17", "@storybook/addon-interactions": "^7.6.17", "@storybook/addon-links": "^7.6.17", @@ -31,10 +46,13 @@ "@types/node": "^20.2.5", "@types/react": "^18.2.8", "@types/react-dom": "^18.2.4", + "@types/redux-persist": "^4.3.1", "@typescript-eslint/eslint-plugin": "^5.59.9", "@typescript-eslint/parser": "^5.59.9", "babel-loader": "^9.1.2", + "bootstrap": "^5.3.3", "clean-webpack-plugin": "^4.0.0", + "copy-webpack-plugin": "^12.0.2", "css-loader": "^6.8.1", "eslint": "8.22.0", "eslint-config-airbnb": "^19.0.4", @@ -46,6 +64,7 @@ "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-storybook": "^0.8.0", "fork-ts-checker-webpack-plugin": "^8.0.0", + "gh-pages": "^6.1.1", "html-webpack-plugin": "^5.5.1", "husky": "^8.0.0", "jest": "^29.5.0", @@ -53,6 +72,10 @@ "less-loader": "^11.1.2", "mini-css-extract-plugin": "^2.7.6", "prettier": "2.8.8", + "react-redux": "^9.1.2", + "react-router": "^7.0.1", + "react-router-dom": "^7.0.1", + "redux-persist": "^6.0.0", "sass": "^1.71.1", "sass-loader": "^13.3.1", "storybook": "^7.6.17", @@ -60,7 +83,7 @@ "ts-jest": "^29.1.0", "typescript": "^5.1.3", "webpack": "^5.85.0", - "webpack-cli": "^5.1.3", + "webpack-cli": "^5.1.4", "webpack-dev-server": "^4.15.0" } }, @@ -2032,12 +2055,12 @@ "dev": true }, "node_modules/@babel/runtime": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.5.tgz", - "integrity": "sha512-ecjvYlnAaZ/KVneE/OdKYBYfgXV3Ptu6zQWmgEF7vwKhQnvVS6bjMD2XYgj+SNvQ1GfK/pjgokfPkC/2CO8CuA==", - "dev": true, + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.7.tgz", + "integrity": "sha512-FjoyLe754PMiYsFaN5C94ttGiOmBNYTf6pLr4xXHAT5uctHb092PBszndLDR5XA/jghQvn4n7JMHl7dmTgbm9w==", + "license": "MIT", "dependencies": { - "regenerator-runtime": "^0.13.11" + "regenerator-runtime": "^0.14.0" }, "engines": { "node": ">=6.9.0" @@ -2576,6 +2599,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@faker-js/faker": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.0.2.tgz", + "integrity": "sha512-nI/FP30ZGXb+UaR7yXawVTH40NVKXPIx0tA3GKjkKLjorqBoMAeq4iSEacl8mJmpVhOCDa0vYHwYDmOOcFMrYw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/fakerjs" + } + ], + "license": "MIT", + "engines": { + "node": ">=18.0.0", + "npm": ">=9.0.0" + } + }, "node_modules/@fal-works/esbuild-plugin-global-externals": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/@fal-works/esbuild-plugin-global-externals/-/esbuild-plugin-global-externals-2.1.2.tgz", @@ -2620,6 +2660,76 @@ "integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==", "dev": true }, + "node_modules/@fortawesome/fontawesome-common-types": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.6.0.tgz", + "integrity": "sha512-xyX0X9mc0kyz9plIyryrRbl7ngsA9jz77mCZJsUkLl+ZKs0KWObgaEBoSgQiYWAsSmjz/yjl0F++Got0Mdp4Rw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/fontawesome-svg-core": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.6.0.tgz", + "integrity": "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg==", + "license": "MIT", + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-brands-svg-icons": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.6.0.tgz", + "integrity": "sha512-1MPD8lMNW/earme4OQi1IFHtmHUwAKgghXlNwWi9GO7QkTfD+IIaYpIai4m2YJEzqfEji3jFHX1DZI5pbY/biQ==", + "license": "(CC-BY-4.0 AND MIT)", + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-regular-svg-icons": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.6.0.tgz", + "integrity": "sha512-Yv9hDzL4aI73BEwSEh20clrY8q/uLxawaQ98lekBx6t9dQKDHcDzzV1p2YtBGTtolYtNqcWdniOnhzB+JPnQEQ==", + "license": "(CC-BY-4.0 AND MIT)", + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-solid-svg-icons": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.6.0.tgz", + "integrity": "sha512-IYv/2skhEDFc2WGUcqvFJkeK39Q+HyPf5GHUrT/l2pKbtgEIv1al1TKd6qStR5OIwQdN1GZP54ci3y4mroJWjA==", + "license": "(CC-BY-4.0 AND MIT)", + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/react-fontawesome": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.2.tgz", + "integrity": "sha512-EnkrprPNqI6SXJl//m29hpaNzOp1bruISWaOiRtkMi/xSvHJlzc2j2JAYS7egxt/EbjSNV/k6Xy0AQI6vB2+1g==", + "license": "MIT", + "dependencies": { + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "@fortawesome/fontawesome-svg-core": "~1 || ~6", + "react": ">=16.3" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.10.7", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.10.7.tgz", @@ -3654,6 +3764,18 @@ } } }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, "node_modules/@radix-ui/number": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.0.1.tgz", @@ -4317,12 +4439,50 @@ "@babel/runtime": "^7.13.10" } }, + "node_modules/@reduxjs/toolkit": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.4.0.tgz", + "integrity": "sha512-wJZEuSKj14tvNfxiIiJws0tQN77/rDqucBq528ApebMIRHyWpCanJVQRxQ8WWZC19iCDKxDsGlbAir3F1layxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "immer": "^10.0.3", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, "node_modules/@sinclair/typebox": { "version": "0.25.24", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz", "integrity": "sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==", "dev": true }, + "node_modules/@sindresorhus/merge-streams": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", + "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@sinonjs/commons": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", @@ -7520,6 +7680,13 @@ "@types/node": "*" } }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/cross-spawn": { "version": "6.0.6", "resolved": "https://registry.npmjs.org/@types/cross-spawn/-/cross-spawn-6.0.6.tgz", @@ -7851,13 +8018,13 @@ "dev": true }, "node_modules/@types/react": { - "version": "18.2.9", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.9.tgz", - "integrity": "sha512-pL3JAesUkF7PEQGxh5XOwdXGV907te6m1/Qe1ERJLgomojS6Ne790QiA7GUl434JEkFA2aAaB6qJ5z4e1zJn/w==", + "version": "18.3.12", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.12.tgz", + "integrity": "sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==", "dev": true, + "license": "MIT", "dependencies": { "@types/prop-types": "*", - "@types/scheduler": "*", "csstype": "^3.0.2" } }, @@ -7870,6 +8037,17 @@ "@types/react": "*" } }, + "node_modules/@types/redux-persist": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/redux-persist/-/redux-persist-4.3.1.tgz", + "integrity": "sha512-YkMnMUk+4//wPtiSTMfsxST/F9Gh9sPWX0LVxHuOidGjojHtMdpep2cYvQgfiDMnj34orXyZI+QJCQMZDlafKA==", + "deprecated": "This is a stub types definition for redux-persist (https://github.com/rt2zz/redux-persist). redux-persist provides its own type definitions, so you don't need @types/redux-persist installed!", + "dev": true, + "license": "MIT", + "dependencies": { + "redux-persist": "*" + } + }, "node_modules/@types/resolve": { "version": "1.20.6", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.6.tgz", @@ -7882,12 +8060,6 @@ "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", "dev": true }, - "node_modules/@types/scheduler": { - "version": "0.16.3", - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", - "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==", - "dev": true - }, "node_modules/@types/semver": { "version": "7.5.0", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.0.tgz", @@ -7950,6 +8122,13 @@ "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==", "dev": true }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", + "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/uuid": { "version": "9.0.8", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", @@ -9448,6 +9627,26 @@ "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", "dev": true }, + "node_modules/bootstrap": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.3.tgz", + "integrity": "sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/twbs" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + } + ], + "license": "MIT", + "peerDependencies": { + "@popperjs/core": "^2.11.8" + } + }, "node_modules/bplist-parser": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.2.0.tgz", @@ -10214,6 +10413,135 @@ "url": "https://github.com/sponsors/mesqueeb" } }, + "node_modules/copy-webpack-plugin": { + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-12.0.2.tgz", + "integrity": "sha512-SNwdBeHyII+rWvee/bTnAYyO8vfVdcSTud4EIb6jcZ8inLeWucJE0DnxXQBjlQ5zlteuuvooGQy3LIyGxhvlOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.1", + "globby": "^14.0.0", + "normalize-path": "^3.0.0", + "schema-utils": "^4.2.0", + "serialize-javascript": "^6.0.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + } + }, + "node_modules/copy-webpack-plugin/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/copy-webpack-plugin/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/copy-webpack-plugin/node_modules/globby": { + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.0.2.tgz", + "integrity": "sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^2.1.0", + "fast-glob": "^3.3.2", + "ignore": "^5.2.4", + "path-type": "^5.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/copy-webpack-plugin/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/copy-webpack-plugin/node_modules/path-type": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-5.0.0.tgz", + "integrity": "sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/copy-webpack-plugin/node_modules/schema-utils": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/copy-webpack-plugin/node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/core-js-compat": { "version": "3.33.1", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.33.1.tgz", @@ -10260,6 +10588,15 @@ "node": ">=10" } }, + "node_modules/cross-fetch": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz", + "integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==", + "license": "MIT", + "dependencies": { + "node-fetch": "^2.6.12" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -11021,6 +11358,13 @@ "integrity": "sha512-8KR114CAYQ4/r5EIEsOmOMqQ9j0MRbJZR3aXD/KFA8RuKzyoUB4XrUCg+l8RUGqTVQgKNIgTpjaG8YHRPAbX2w==", "dev": true }, + "node_modules/email-addresses": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/email-addresses/-/email-addresses-5.0.0.tgz", + "integrity": "sha512-4OIPYlA6JXqtVn8zpHpGiI7vE6EQOAg16aGnDMIAlZVinnoZ8208tW1hAbjWydgN/4PLTT9q+O1K6AH/vALJGw==", + "dev": true, + "license": "MIT" + }, "node_modules/emittery": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", @@ -12360,10 +12704,11 @@ "dev": true }, "node_modules/fast-glob": { - "version": "3.2.12", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", - "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -12405,6 +12750,13 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "node_modules/fast-uri": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz", + "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/fastest-levenshtein": { "version": "1.0.16", "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", @@ -12511,6 +12863,34 @@ "node": ">=10" } }, + "node_modules/filename-reserved-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz", + "integrity": "sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/filenamify": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-4.3.0.tgz", + "integrity": "sha512-hcFKyUG57yWGAzu1CMt/dPzYZuv+jAJUT85bL8mrXvNe6hWj6yEHEc4EdcgiA6Z3oi1/9wXJdZPXF2dZNgwgOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "filename-reserved-regex": "^2.0.0", + "strip-outer": "^1.0.1", + "trim-repeated": "^1.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -13147,6 +13527,101 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gh-pages": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/gh-pages/-/gh-pages-6.1.1.tgz", + "integrity": "sha512-upnohfjBwN5hBP9w2dPE7HO5JJTHzSGMV1JrLrHvNuqmjoYHg6TBrCcnEoorjG/e0ejbuvnwyKMdTyM40PEByw==", + "dev": true, + "license": "MIT", + "dependencies": { + "async": "^3.2.4", + "commander": "^11.0.0", + "email-addresses": "^5.0.0", + "filenamify": "^4.3.0", + "find-cache-dir": "^3.3.1", + "fs-extra": "^11.1.1", + "globby": "^6.1.0" + }, + "bin": { + "gh-pages": "bin/gh-pages.js", + "gh-pages-clean": "bin/gh-pages-clean.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/gh-pages/node_modules/array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-uniq": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gh-pages/node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/gh-pages/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gh-pages/node_modules/globby": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", + "integrity": "sha512-KVbFv2TQtbzCoxAnfD6JcHZTYCzyliEaaeM/gH8qQdkKr5s0OP9scEgvdcngyk7AVdY6YVW/TJHd+lQ/Df3Daw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^1.0.1", + "glob": "^7.0.3", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gh-pages/node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/giget": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/giget/-/giget-1.2.1.tgz", @@ -13546,6 +14021,15 @@ "node": ">=12" } }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "license": "MIT", + "dependencies": { + "void-elements": "3.1.0" + } + }, "node_modules/html-tags": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz", @@ -13726,6 +14210,47 @@ "url": "https://github.com/sponsors/typicode" } }, + "node_modules/i18next": { + "version": "23.15.2", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.15.2.tgz", + "integrity": "sha512-zcPSWzCvw6uKnuYHIqs4W7hTuB9e3AFcSdZgvCWoPXIZsBjBd4djN2/2uOHIB+1DFFkQnMBXvhNg7J3WyCuywQ==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, + "node_modules/i18next-browser-languagedetector": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.0.0.tgz", + "integrity": "sha512-zhXdJXTTCoG39QsrOCiOabnWj2jecouOqbchu3EfhtSHxIB5Uugnm9JaizenOy39h7ne3+fLikIjeW88+rgszw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, + "node_modules/i18next-http-backend": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-2.6.2.tgz", + "integrity": "sha512-Hp/kd8/VuoxIHmxsknJXjkTYYHzivAyAF15pzliKzk2TiXC25rZCEerb1pUFoxz4IVrG3fCvQSY51/Lu4ECV4A==", + "license": "MIT", + "dependencies": { + "cross-fetch": "4.0.0" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -13793,6 +14318,17 @@ "node": ">=0.10.0" } }, + "node_modules/immer": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz", + "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/immutable": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.4.tgz", @@ -13955,6 +14491,12 @@ "node": ">= 0.4" } }, + "node_modules/intersection-observer": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/intersection-observer/-/intersection-observer-0.12.2.tgz", + "integrity": "sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg==", + "license": "Apache-2.0" + }, "node_modules/invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", @@ -18333,6 +18875,12 @@ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", "dev": true }, + "node_modules/module-alias": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/module-alias/-/module-alias-2.2.3.tgz", + "integrity": "sha512-23g5BFj4zdQL/b6tor7Ji+QY4pEfNH784BMslY9Qb0UnJWRAt+lQGLYmRaM0KDBwIG23ffEBELhZDP2rhi9f/Q==", + "license": "MIT" + }, "node_modules/mri": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", @@ -18482,7 +19030,6 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "dev": true, "dependencies": { "whatwg-url": "^5.0.0" }, @@ -18507,20 +19054,17 @@ "node_modules/node-fetch/node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "dev": true + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, "node_modules/node-fetch/node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "dev": true + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, "node_modules/node-fetch/node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dev": true, "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" @@ -18763,7 +19307,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -19601,7 +20144,6 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -19611,8 +20153,7 @@ "node_modules/prop-types/node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, "node_modules/proxy-addr": { "version": "2.0.7", @@ -19941,12 +20482,89 @@ "integrity": "sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg==", "dev": true }, + "node_modules/react-hook-form": { + "version": "7.53.2", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.53.2.tgz", + "integrity": "sha512-YVel6fW5sOeedd1524pltpHX+jgU2u3DSDtXEaBORNdqiNrsX/nUI/iGXONegttg0mJVnfrIkiV0cmTU6Oo2xw==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, + "node_modules/react-i18next": { + "version": "15.0.2", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.0.2.tgz", + "integrity": "sha512-z0W3/RES9Idv3MmJUcf0mDNeeMOUXe+xoL0kPfQPbDoZHmni/XsIoq5zgT2MCFUiau283GuBUK578uD/mkAbLQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.0", + "html-parse-stringify": "^3.0.1" + }, + "peerDependencies": { + "i18next": ">= 23.2.3", + "react": ">= 16.8.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/react-intersection-observer": { + "version": "9.13.1", + "resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-9.13.1.tgz", + "integrity": "sha512-tSzDaTy0qwNPLJHg8XZhlyHTgGW6drFKTtvjdL+p6um12rcnp8Z5XstE+QNBJ7c64n5o0Lj4ilUleA41bmDoMw==", + "license": "MIT", + "peerDependencies": { + "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true }, + "node_modules/react-redux": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.1.2.tgz", + "integrity": "sha512-0OA4dhM1W48l3uzmv6B7TXPCGmokUU4p1M44DGN2/D9a1FjVPukVjER1PcPX97jIg6aUeLq1XJo1IpfbgULn0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.3", + "use-sync-external-store": "^1.0.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25", + "react": "^18.0", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, "node_modules/react-refresh": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", @@ -20003,6 +20621,58 @@ } } }, + "node_modules/react-router": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.0.1.tgz", + "integrity": "sha512-WVAhv9oWCNsja5AkK6KLpXJDSJCQizOIyOd4vvB/+eHGbYx5vkhcmcmwWjQ9yqkRClogi+xjEg9fNEOd5EX/tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/cookie": "^0.6.0", + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0", + "turbo-stream": "2.4.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.0.1.tgz", + "integrity": "sha512-duBzwAAiIabhFPZfDjcYpJ+f08TMbPMETgq254GWne2NW1ZwRHhZLj7tpSp8KGb7JvZzlLcjGUnqLxpZQVEPng==", + "dev": true, + "license": "MIT", + "dependencies": { + "react-router": "7.0.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/react-router/node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/react-style-singleton": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz", @@ -20179,6 +20849,33 @@ "node": ">=0.10.0" } }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/redux-persist": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/redux-persist/-/redux-persist-6.0.0.tgz", + "integrity": "sha512-71LLMbUq2r02ng2We9S215LtPu3fY0KgaGE0k8WRgl6RkqxtGfl7HUozz1Dftwsb0D/5mZ8dwAaPbtnzfvbEwQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "redux": ">4.0.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, "node_modules/regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -20198,10 +20895,10 @@ } }, "node_modules/regenerator-runtime": { - "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 + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "license": "MIT" }, "node_modules/regenerator-transform": { "version": "0.15.2", @@ -20382,6 +21079,13 @@ "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", "dev": true }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "dev": true, + "license": "MIT" + }, "node_modules/resolve": { "version": "1.22.2", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", @@ -20742,10 +21446,11 @@ "dev": true }, "node_modules/serialize-javascript": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", - "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "randombytes": "^2.1.0" } @@ -20843,6 +21548,13 @@ "node": ">= 0.8.0" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "dev": true, + "license": "MIT" + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -21335,6 +22047,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strip-outer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz", + "integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/style-loader": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.3.tgz", @@ -21964,6 +22689,19 @@ "node": ">=12" } }, + "node_modules/trim-repeated": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", + "integrity": "sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/trough": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", @@ -22128,6 +22866,13 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "dev": true }, + "node_modules/turbo-stream": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz", + "integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==", + "dev": true, + "license": "ISC" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -22287,6 +23032,19 @@ "node": ">=4" } }, + "node_modules/unicorn-magic": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", + "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/unified": { "version": "10.1.2", "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", @@ -22560,6 +23318,16 @@ } } }, + "node_modules/use-sync-external-store": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz", + "integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/util": { "version": "0.12.5", "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", @@ -22703,6 +23471,15 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/w3c-xmlserializer": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", @@ -22816,6 +23593,7 @@ "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", "dev": true, + "license": "MIT", "dependencies": { "@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/configtest": "^2.1.1", @@ -24908,12 +25686,11 @@ "dev": true }, "@babel/runtime": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.5.tgz", - "integrity": "sha512-ecjvYlnAaZ/KVneE/OdKYBYfgXV3Ptu6zQWmgEF7vwKhQnvVS6bjMD2XYgj+SNvQ1GfK/pjgokfPkC/2CO8CuA==", - "dev": true, + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.7.tgz", + "integrity": "sha512-FjoyLe754PMiYsFaN5C94ttGiOmBNYTf6pLr4xXHAT5uctHb092PBszndLDR5XA/jghQvn4n7JMHl7dmTgbm9w==", "requires": { - "regenerator-runtime": "^0.13.11" + "regenerator-runtime": "^0.14.0" } }, "@babel/template": { @@ -25206,6 +25983,12 @@ } } }, + "@faker-js/faker": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.0.2.tgz", + "integrity": "sha512-nI/FP30ZGXb+UaR7yXawVTH40NVKXPIx0tA3GKjkKLjorqBoMAeq4iSEacl8mJmpVhOCDa0vYHwYDmOOcFMrYw==", + "dev": true + }, "@fal-works/esbuild-plugin-global-externals": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/@fal-works/esbuild-plugin-global-externals/-/esbuild-plugin-global-externals-2.1.2.tgz", @@ -25246,6 +26029,51 @@ "integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==", "dev": true }, + "@fortawesome/fontawesome-common-types": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.6.0.tgz", + "integrity": "sha512-xyX0X9mc0kyz9plIyryrRbl7ngsA9jz77mCZJsUkLl+ZKs0KWObgaEBoSgQiYWAsSmjz/yjl0F++Got0Mdp4Rw==" + }, + "@fortawesome/fontawesome-svg-core": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.6.0.tgz", + "integrity": "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg==", + "requires": { + "@fortawesome/fontawesome-common-types": "6.6.0" + } + }, + "@fortawesome/free-brands-svg-icons": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.6.0.tgz", + "integrity": "sha512-1MPD8lMNW/earme4OQi1IFHtmHUwAKgghXlNwWi9GO7QkTfD+IIaYpIai4m2YJEzqfEji3jFHX1DZI5pbY/biQ==", + "requires": { + "@fortawesome/fontawesome-common-types": "6.6.0" + } + }, + "@fortawesome/free-regular-svg-icons": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.6.0.tgz", + "integrity": "sha512-Yv9hDzL4aI73BEwSEh20clrY8q/uLxawaQ98lekBx6t9dQKDHcDzzV1p2YtBGTtolYtNqcWdniOnhzB+JPnQEQ==", + "requires": { + "@fortawesome/fontawesome-common-types": "6.6.0" + } + }, + "@fortawesome/free-solid-svg-icons": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.6.0.tgz", + "integrity": "sha512-IYv/2skhEDFc2WGUcqvFJkeK39Q+HyPf5GHUrT/l2pKbtgEIv1al1TKd6qStR5OIwQdN1GZP54ci3y4mroJWjA==", + "requires": { + "@fortawesome/fontawesome-common-types": "6.6.0" + } + }, + "@fortawesome/react-fontawesome": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.2.tgz", + "integrity": "sha512-EnkrprPNqI6SXJl//m29hpaNzOp1bruISWaOiRtkMi/xSvHJlzc2j2JAYS7egxt/EbjSNV/k6Xy0AQI6vB2+1g==", + "requires": { + "prop-types": "^15.8.1" + } + }, "@humanwhocodes/config-array": { "version": "0.10.7", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.10.7.tgz", @@ -26010,6 +26838,13 @@ "source-map": "^0.7.3" } }, + "@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "dev": true, + "peer": true + }, "@radix-ui/number": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.0.1.tgz", @@ -26360,12 +27195,30 @@ "@babel/runtime": "^7.13.10" } }, + "@reduxjs/toolkit": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.4.0.tgz", + "integrity": "sha512-wJZEuSKj14tvNfxiIiJws0tQN77/rDqucBq528ApebMIRHyWpCanJVQRxQ8WWZC19iCDKxDsGlbAir3F1layxA==", + "dev": true, + "requires": { + "immer": "^10.0.3", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + } + }, "@sinclair/typebox": { "version": "0.25.24", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz", "integrity": "sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==", "dev": true }, + "@sindresorhus/merge-streams": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", + "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", + "dev": true + }, "@sinonjs/commons": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", @@ -28752,6 +29605,12 @@ "@types/node": "*" } }, + "@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "dev": true + }, "@types/cross-spawn": { "version": "6.0.6", "resolved": "https://registry.npmjs.org/@types/cross-spawn/-/cross-spawn-6.0.6.tgz", @@ -29076,13 +29935,12 @@ "dev": true }, "@types/react": { - "version": "18.2.9", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.9.tgz", - "integrity": "sha512-pL3JAesUkF7PEQGxh5XOwdXGV907te6m1/Qe1ERJLgomojS6Ne790QiA7GUl434JEkFA2aAaB6qJ5z4e1zJn/w==", + "version": "18.3.12", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.12.tgz", + "integrity": "sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==", "dev": true, "requires": { "@types/prop-types": "*", - "@types/scheduler": "*", "csstype": "^3.0.2" } }, @@ -29095,6 +29953,15 @@ "@types/react": "*" } }, + "@types/redux-persist": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/redux-persist/-/redux-persist-4.3.1.tgz", + "integrity": "sha512-YkMnMUk+4//wPtiSTMfsxST/F9Gh9sPWX0LVxHuOidGjojHtMdpep2cYvQgfiDMnj34orXyZI+QJCQMZDlafKA==", + "dev": true, + "requires": { + "redux-persist": "*" + } + }, "@types/resolve": { "version": "1.20.6", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.6.tgz", @@ -29107,12 +29974,6 @@ "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", "dev": true }, - "@types/scheduler": { - "version": "0.16.3", - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", - "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==", - "dev": true - }, "@types/semver": { "version": "7.5.0", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.0.tgz", @@ -29175,6 +30036,12 @@ "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==", "dev": true }, + "@types/use-sync-external-store": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", + "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==", + "dev": true + }, "@types/uuid": { "version": "9.0.8", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", @@ -30318,6 +31185,13 @@ "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", "dev": true }, + "bootstrap": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.3.tgz", + "integrity": "sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg==", + "dev": true, + "requires": {} + }, "bplist-parser": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.2.0.tgz", @@ -30889,6 +31763,87 @@ "is-what": "^3.14.1" } }, + "copy-webpack-plugin": { + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-12.0.2.tgz", + "integrity": "sha512-SNwdBeHyII+rWvee/bTnAYyO8vfVdcSTud4EIb6jcZ8inLeWucJE0DnxXQBjlQ5zlteuuvooGQy3LIyGxhvlOA==", + "dev": true, + "requires": { + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.1", + "globby": "^14.0.0", + "normalize-path": "^3.0.0", + "schema-utils": "^4.2.0", + "serialize-javascript": "^6.0.2" + }, + "dependencies": { + "ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + } + }, + "ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.3" + } + }, + "globby": { + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.0.2.tgz", + "integrity": "sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==", + "dev": true, + "requires": { + "@sindresorhus/merge-streams": "^2.1.0", + "fast-glob": "^3.3.2", + "ignore": "^5.2.4", + "path-type": "^5.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.1.0" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "path-type": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-5.0.0.tgz", + "integrity": "sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==", + "dev": true + }, + "schema-utils": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + } + }, + "slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "dev": true + } + } + }, "core-js-compat": { "version": "3.33.1", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.33.1.tgz", @@ -30923,6 +31878,14 @@ "yaml": "^1.10.0" } }, + "cross-fetch": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz", + "integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==", + "requires": { + "node-fetch": "^2.6.12" + } + }, "cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -31513,6 +32476,12 @@ "integrity": "sha512-8KR114CAYQ4/r5EIEsOmOMqQ9j0MRbJZR3aXD/KFA8RuKzyoUB4XrUCg+l8RUGqTVQgKNIgTpjaG8YHRPAbX2w==", "dev": true }, + "email-addresses": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/email-addresses/-/email-addresses-5.0.0.tgz", + "integrity": "sha512-4OIPYlA6JXqtVn8zpHpGiI7vE6EQOAg16aGnDMIAlZVinnoZ8208tW1hAbjWydgN/4PLTT9q+O1K6AH/vALJGw==", + "dev": true + }, "emittery": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", @@ -32508,9 +33477,9 @@ "dev": true }, "fast-glob": { - "version": "3.2.12", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", - "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", "dev": true, "requires": { "@nodelib/fs.stat": "^2.0.2", @@ -32549,6 +33518,12 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "fast-uri": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz", + "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==", + "dev": true + }, "fastest-levenshtein": { "version": "1.0.16", "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", @@ -32645,6 +33620,23 @@ } } }, + "filename-reserved-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz", + "integrity": "sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ==", + "dev": true + }, + "filenamify": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-4.3.0.tgz", + "integrity": "sha512-hcFKyUG57yWGAzu1CMt/dPzYZuv+jAJUT85bL8mrXvNe6hWj6yEHEc4EdcgiA6Z3oi1/9wXJdZPXF2dZNgwgOg==", + "dev": true, + "requires": { + "filename-reserved-regex": "^2.0.0", + "strip-outer": "^1.0.1", + "trim-repeated": "^1.0.0" + } + }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -33105,6 +34097,71 @@ "get-intrinsic": "^1.1.1" } }, + "gh-pages": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/gh-pages/-/gh-pages-6.1.1.tgz", + "integrity": "sha512-upnohfjBwN5hBP9w2dPE7HO5JJTHzSGMV1JrLrHvNuqmjoYHg6TBrCcnEoorjG/e0ejbuvnwyKMdTyM40PEByw==", + "dev": true, + "requires": { + "async": "^3.2.4", + "commander": "^11.0.0", + "email-addresses": "^5.0.0", + "filenamify": "^4.3.0", + "find-cache-dir": "^3.3.1", + "fs-extra": "^11.1.1", + "globby": "^6.1.0" + }, + "dependencies": { + "array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng==", + "dev": true, + "requires": { + "array-uniq": "^1.0.1" + } + }, + "commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "dev": true + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "globby": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", + "integrity": "sha512-KVbFv2TQtbzCoxAnfD6JcHZTYCzyliEaaeM/gH8qQdkKr5s0OP9scEgvdcngyk7AVdY6YVW/TJHd+lQ/Df3Daw==", + "dev": true, + "requires": { + "array-union": "^1.0.1", + "glob": "^7.0.3", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true + } + } + }, "giget": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/giget/-/giget-1.2.1.tgz", @@ -33415,6 +34472,14 @@ "terser": "^5.10.0" } }, + "html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "requires": { + "void-elements": "3.1.0" + } + }, "html-tags": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz", @@ -33536,6 +34601,30 @@ "integrity": "sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==", "dev": true }, + "i18next": { + "version": "23.15.2", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.15.2.tgz", + "integrity": "sha512-zcPSWzCvw6uKnuYHIqs4W7hTuB9e3AFcSdZgvCWoPXIZsBjBd4djN2/2uOHIB+1DFFkQnMBXvhNg7J3WyCuywQ==", + "requires": { + "@babel/runtime": "^7.23.2" + } + }, + "i18next-browser-languagedetector": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.0.0.tgz", + "integrity": "sha512-zhXdJXTTCoG39QsrOCiOabnWj2jecouOqbchu3EfhtSHxIB5Uugnm9JaizenOy39h7ne3+fLikIjeW88+rgszw==", + "requires": { + "@babel/runtime": "^7.23.2" + } + }, + "i18next-http-backend": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-2.6.2.tgz", + "integrity": "sha512-Hp/kd8/VuoxIHmxsknJXjkTYYHzivAyAF15pzliKzk2TiXC25rZCEerb1pUFoxz4IVrG3fCvQSY51/Lu4ECV4A==", + "requires": { + "cross-fetch": "4.0.0" + } + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -33572,6 +34661,12 @@ "optional": true, "peer": true }, + "immer": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz", + "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==", + "dev": true + }, "immutable": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.4.tgz", @@ -33693,6 +34788,11 @@ "side-channel": "^1.0.4" } }, + "intersection-observer": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/intersection-observer/-/intersection-observer-0.12.2.tgz", + "integrity": "sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg==" + }, "invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", @@ -36799,6 +37899,11 @@ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", "dev": true }, + "module-alias": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/module-alias/-/module-alias-2.2.3.tgz", + "integrity": "sha512-23g5BFj4zdQL/b6tor7Ji+QY4pEfNH784BMslY9Qb0UnJWRAt+lQGLYmRaM0KDBwIG23ffEBELhZDP2rhi9f/Q==" + }, "mri": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", @@ -36917,7 +38022,6 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "dev": true, "requires": { "whatwg-url": "^5.0.0" }, @@ -36925,20 +38029,17 @@ "tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "dev": true + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, "webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "dev": true + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, "whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dev": true, "requires": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" @@ -37114,8 +38215,7 @@ "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" }, "object-inspect": { "version": "1.12.3", @@ -37715,7 +38815,6 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, "requires": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -37725,8 +38824,7 @@ "react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" } } }, @@ -37986,12 +39084,43 @@ } } }, + "react-hook-form": { + "version": "7.53.2", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.53.2.tgz", + "integrity": "sha512-YVel6fW5sOeedd1524pltpHX+jgU2u3DSDtXEaBORNdqiNrsX/nUI/iGXONegttg0mJVnfrIkiV0cmTU6Oo2xw==", + "requires": {} + }, + "react-i18next": { + "version": "15.0.2", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.0.2.tgz", + "integrity": "sha512-z0W3/RES9Idv3MmJUcf0mDNeeMOUXe+xoL0kPfQPbDoZHmni/XsIoq5zgT2MCFUiau283GuBUK578uD/mkAbLQ==", + "requires": { + "@babel/runtime": "^7.25.0", + "html-parse-stringify": "^3.0.1" + } + }, + "react-intersection-observer": { + "version": "9.13.1", + "resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-9.13.1.tgz", + "integrity": "sha512-tSzDaTy0qwNPLJHg8XZhlyHTgGW6drFKTtvjdL+p6um12rcnp8Z5XstE+QNBJ7c64n5o0Lj4ilUleA41bmDoMw==", + "requires": {} + }, "react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true }, + "react-redux": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.1.2.tgz", + "integrity": "sha512-0OA4dhM1W48l3uzmv6B7TXPCGmokUU4p1M44DGN2/D9a1FjVPukVjER1PcPX97jIg6aUeLq1XJo1IpfbgULn0w==", + "dev": true, + "requires": { + "@types/use-sync-external-store": "^0.0.3", + "use-sync-external-store": "^1.0.0" + } + }, "react-refresh": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", @@ -38021,6 +39150,35 @@ "tslib": "^2.0.0" } }, + "react-router": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.0.1.tgz", + "integrity": "sha512-WVAhv9oWCNsja5AkK6KLpXJDSJCQizOIyOd4vvB/+eHGbYx5vkhcmcmwWjQ9yqkRClogi+xjEg9fNEOd5EX/tw==", + "dev": true, + "requires": { + "@types/cookie": "^0.6.0", + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0", + "turbo-stream": "2.4.0" + }, + "dependencies": { + "cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "dev": true + } + } + }, + "react-router-dom": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.0.1.tgz", + "integrity": "sha512-duBzwAAiIabhFPZfDjcYpJ+f08TMbPMETgq254GWne2NW1ZwRHhZLj7tpSp8KGb7JvZzlLcjGUnqLxpZQVEPng==", + "dev": true, + "requires": { + "react-router": "7.0.1" + } + }, "react-style-singleton": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz", @@ -38149,6 +39307,26 @@ } } }, + "redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "dev": true + }, + "redux-persist": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/redux-persist/-/redux-persist-6.0.0.tgz", + "integrity": "sha512-71LLMbUq2r02ng2We9S215LtPu3fY0KgaGE0k8WRgl6RkqxtGfl7HUozz1Dftwsb0D/5mZ8dwAaPbtnzfvbEwQ==", + "dev": true, + "requires": {} + }, + "redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "dev": true, + "requires": {} + }, "regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -38165,10 +39343,9 @@ } }, "regenerator-runtime": { - "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 + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" }, "regenerator-transform": { "version": "0.15.2", @@ -38306,6 +39483,12 @@ "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", "dev": true }, + "reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "dev": true + }, "resolve": { "version": "1.22.2", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", @@ -38551,9 +39734,9 @@ } }, "serialize-javascript": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", - "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dev": true, "requires": { "randombytes": "^2.1.0" @@ -38639,6 +39822,12 @@ "send": "0.18.0" } }, + "set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "dev": true + }, "setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -39034,6 +40223,15 @@ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true }, + "strip-outer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz", + "integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.2" + } + }, "style-loader": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.3.tgz", @@ -39512,6 +40710,15 @@ "punycode": "^2.1.1" } }, + "trim-repeated": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", + "integrity": "sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg==", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.2" + } + }, "trough": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", @@ -39624,6 +40831,12 @@ } } }, + "turbo-stream": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz", + "integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==", + "dev": true + }, "type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -39737,6 +40950,12 @@ "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", "dev": true }, + "unicorn-magic": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", + "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", + "dev": true + }, "unified": { "version": "10.1.2", "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", @@ -39924,6 +41143,13 @@ "tslib": "^2.0.0" } }, + "use-sync-external-store": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz", + "integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==", + "dev": true, + "requires": {} + }, "util": { "version": "0.12.5", "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", @@ -40036,6 +41262,11 @@ "unist-util-stringify-position": "^3.0.0" } }, + "void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==" + }, "w3c-xmlserializer": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", diff --git a/package.json b/package.json index 492664d1f..bfb38dc34 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,15 @@ { "name": "react-start-template", "version": "1.0.0", + "homepage": "https://yurabox2017.github.io", "description": "Start repo with required configuration", "main": "src/index.tsx", "author": "Igor ", "scripts": { "start": "webpack serve --mode development", "build": "webpack --mode production", + "predeploy": "npm run build", + "deploy": "gh-pages -d dist", "test": "jest src", "lint": "eslint src --fix", "storybook": "storybook dev -p 6006", @@ -18,6 +21,8 @@ "@babel/preset-env": "^7.22.4", "@babel/preset-react": "^7.22.3", "@babel/preset-typescript": "^7.21.5", + "@faker-js/faker": "^9.0.2", + "@reduxjs/toolkit": "^2.4.0", "@storybook/addon-essentials": "^7.6.17", "@storybook/addon-interactions": "^7.6.17", "@storybook/addon-links": "^7.6.17", @@ -31,10 +36,13 @@ "@types/node": "^20.2.5", "@types/react": "^18.2.8", "@types/react-dom": "^18.2.4", + "@types/redux-persist": "^4.3.1", "@typescript-eslint/eslint-plugin": "^5.59.9", "@typescript-eslint/parser": "^5.59.9", "babel-loader": "^9.1.2", + "bootstrap": "^5.3.3", "clean-webpack-plugin": "^4.0.0", + "copy-webpack-plugin": "^12.0.2", "css-loader": "^6.8.1", "eslint": "8.22.0", "eslint-config-airbnb": "^19.0.4", @@ -46,6 +54,7 @@ "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-storybook": "^0.8.0", "fork-ts-checker-webpack-plugin": "^8.0.0", + "gh-pages": "^6.1.1", "html-webpack-plugin": "^5.5.1", "husky": "^8.0.0", "jest": "^29.5.0", @@ -53,6 +62,10 @@ "less-loader": "^11.1.2", "mini-css-extract-plugin": "^2.7.6", "prettier": "2.8.8", + "react-redux": "^9.1.2", + "react-router": "^7.0.1", + "react-router-dom": "^7.0.1", + "redux-persist": "^6.0.0", "sass": "^1.71.1", "sass-loader": "^13.3.1", "storybook": "^7.6.17", @@ -60,12 +73,25 @@ "ts-jest": "^29.1.0", "typescript": "^5.1.3", "webpack": "^5.85.0", - "webpack-cli": "^5.1.3", + "webpack-cli": "^5.1.4", "webpack-dev-server": "^4.15.0" }, "dependencies": { + "@fortawesome/fontawesome-svg-core": "^6.6.0", + "@fortawesome/free-brands-svg-icons": "^6.6.0", + "@fortawesome/free-regular-svg-icons": "^6.6.0", + "@fortawesome/free-solid-svg-icons": "^6.6.0", + "@fortawesome/react-fontawesome": "^0.2.2", "clsx": "^1.2.1", + "i18next": "^23.15.2", + "i18next-browser-languagedetector": "^8.0.0", + "i18next-http-backend": "^2.6.2", + "intersection-observer": "^0.12.2", + "module-alias": "^2.2.3", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "react-hook-form": "^7.53.2", + "react-i18next": "^15.0.2", + "react-intersection-observer": "^9.13.1" } } diff --git a/src/app/App.css b/src/app/App.css index 78b8850cf..1d04523ef 100644 --- a/src/app/App.css +++ b/src/app/App.css @@ -1,38 +1,66 @@ .App { - text-align: center; + text-align: center; + height: 100vh; } .App-logo { - height: 40vmin; - pointer-events: none; + height: 40vmin; + pointer-events: none; +} + +.text-left { + text-align: left !important; } @media (prefers-reduced-motion: no-preference) { - .App-logo { - animation: App-logo-spin infinite 20s linear; - } + .App-logo { + animation: App-logo-spin infinite 20s linear; + } } .App-header { - background-color: #282c34; - min-height: 100vh; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - font-size: calc(10px + 2vmin); - color: white; + background-color: #282c34; + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + font-size: calc(10px + 2vmin); + color: white; +} + +.bg-light { + background-color: #f0f0f0 !important; + color: #282c34 !important; +} + +.bg-dark { + background-color: #282c34 !important; + color: white !important; } +.navbar.bg-dark { + border-bottom: 1px solid white; +} +.navbar.bg-light { + border-bottom: 1px solid black; +} +.navbar.bg-dark .nav-link { + color: white !important; +} +.navbar.bg-light .nav-link { + color: black !important; +} .App-link { - color: #61dafb; + color: #61dafb; } @keyframes App-logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } + from { + transform: rotate(0deg); + } + + to { + transform: rotate(360deg); + } } diff --git a/src/app/App.tsx b/src/app/App.tsx index dcc0ff8ad..1f8fb73d2 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -1,17 +1,40 @@ -import React from 'react'; -import logo from './logo.svg'; +import React, { useEffect, useState } from 'react'; import './App.css'; +import { Theme, ThemeContext } from 'src/context/themeContext'; +import { LocalizationInitiator } from 'src/localization/LocalizationInitiator'; +import { I18nextProvider } from 'react-i18next'; +import i18n from 'src/localization/settings'; +import { CustomRoutes } from 'src/routes/routes'; function App() { + const [theme, setTheme] = useState(() => 'light'); + + const setHtmlAttribute = () => { + const html = document.querySelector('html'); + if (html) html.setAttribute('data-bs-theme', theme); + }; + + useEffect(() => { + setHtmlAttribute(); + }, [theme]); + + const toggleTheme = () => { + setTheme((prevTheme) => (prevTheme === 'light' ? 'dark' : 'light')); + setHtmlAttribute(); + }; + return ( -
-
- logo -

- Текст писать тут -

-
-
+ <> + + + +
+ + {/* */} +
+
+
+ ); } diff --git a/src/assets/businessman (1).png b/src/assets/businessman (1).png new file mode 100644 index 000000000..0dc75d24e Binary files /dev/null and b/src/assets/businessman (1).png differ diff --git a/src/assets/businessman.png b/src/assets/businessman.png new file mode 100644 index 000000000..96f68193c Binary files /dev/null and b/src/assets/businessman.png differ diff --git a/src/assets/cleaner.png b/src/assets/cleaner.png new file mode 100644 index 000000000..3c0f06fe0 Binary files /dev/null and b/src/assets/cleaner.png differ diff --git a/src/assets/free-icon-cleaning-9012135.png b/src/assets/free-icon-cleaning-9012135.png new file mode 100644 index 000000000..de7edd177 Binary files /dev/null and b/src/assets/free-icon-cleaning-9012135.png differ diff --git a/src/assets/free-icon-cleaning-9717764.png b/src/assets/free-icon-cleaning-9717764.png new file mode 100644 index 000000000..18e0305e8 Binary files /dev/null and b/src/assets/free-icon-cleaning-9717764.png differ diff --git a/src/assets/free-icon-household-7029117.png b/src/assets/free-icon-household-7029117.png new file mode 100644 index 000000000..c27f94814 Binary files /dev/null and b/src/assets/free-icon-household-7029117.png differ diff --git a/src/assets/logo-clean.png b/src/assets/logo-clean.png new file mode 100644 index 000000000..100fe0a13 Binary files /dev/null and b/src/assets/logo-clean.png differ diff --git a/src/assets/logo-image.png b/src/assets/logo-image.png new file mode 100644 index 000000000..6fc41c902 Binary files /dev/null and b/src/assets/logo-image.png differ diff --git a/src/assets/logo-image32.png b/src/assets/logo-image32.png new file mode 100644 index 000000000..dcc566e73 Binary files /dev/null and b/src/assets/logo-image32.png differ diff --git a/src/assets/logo-image64.png b/src/assets/logo-image64.png new file mode 100644 index 000000000..0037316ad Binary files /dev/null and b/src/assets/logo-image64.png differ diff --git a/src/assets/mop and broom.png b/src/assets/mop and broom.png new file mode 100644 index 000000000..721c39c79 Binary files /dev/null and b/src/assets/mop and broom.png differ diff --git a/src/assets/yoga-mat-cleaning-kit.png b/src/assets/yoga-mat-cleaning-kit.png new file mode 100644 index 000000000..a824d7a6d Binary files /dev/null and b/src/assets/yoga-mat-cleaning-kit.png differ diff --git a/src/context/themeContext.ts b/src/context/themeContext.ts new file mode 100644 index 000000000..2feca76fb --- /dev/null +++ b/src/context/themeContext.ts @@ -0,0 +1,10 @@ +import { createContext } from 'react'; + +export type Theme = 'light' | 'dark'; + +export interface ThemeContextType { + theme: Theme; + toggleTheme: () => void; +} + +export const ThemeContext = createContext({} as ThemeContextType); diff --git a/src/entities/interfaces/ICartButton.ts b/src/entities/interfaces/ICartButton.ts new file mode 100644 index 000000000..b02dc073a --- /dev/null +++ b/src/entities/interfaces/ICartButton.ts @@ -0,0 +1,8 @@ +import { ICartProduct } from './ICartProduct'; + +export default interface ICartButton { + product: ICartProduct; + // addProduct: () => void; + // increment: () => void; + // decrement: () => void; +} diff --git a/src/entities/interfaces/ICartProduct.ts b/src/entities/interfaces/ICartProduct.ts new file mode 100644 index 000000000..2b9fe814a --- /dev/null +++ b/src/entities/interfaces/ICartProduct.ts @@ -0,0 +1,3 @@ +import IShortProduct from './IShortProduct'; + +export interface ICartProduct extends IShortProduct {} diff --git a/src/entities/interfaces/IFullProduct.ts b/src/entities/interfaces/IFullProduct.ts new file mode 100644 index 000000000..d19182f2f --- /dev/null +++ b/src/entities/interfaces/IFullProduct.ts @@ -0,0 +1,7 @@ +export default interface IFullProduct { + price: number; + images: string[]; + category: string; + title: string; + description: string; +} diff --git a/src/entities/interfaces/IFuncProps.ts b/src/entities/interfaces/IFuncProps.ts new file mode 100644 index 000000000..d7f72893f --- /dev/null +++ b/src/entities/interfaces/IFuncProps.ts @@ -0,0 +1,6 @@ +import IFullProduct from './IFullProduct'; +import IShortProduct from './IShortProduct'; + +export default interface FuncProps { + products: IShortProduct[]; +} diff --git a/src/entities/interfaces/IModal.ts b/src/entities/interfaces/IModal.ts new file mode 100644 index 000000000..77a26e687 --- /dev/null +++ b/src/entities/interfaces/IModal.ts @@ -0,0 +1,8 @@ +import React from 'react'; + +export default interface IModal { + children?: React.ReactNode; + visible: boolean; + header: string; + onClose: () => void; +} diff --git a/src/entities/interfaces/IShortProduct.ts b/src/entities/interfaces/IShortProduct.ts new file mode 100644 index 000000000..c29487937 --- /dev/null +++ b/src/entities/interfaces/IShortProduct.ts @@ -0,0 +1,8 @@ +export default interface IShortProduct { + id: number; + category: string; + title: string; + price: number; + description: string; + image: string; +} diff --git a/src/entities/interfaces/IUserProfile.ts b/src/entities/interfaces/IUserProfile.ts new file mode 100644 index 000000000..024f38826 --- /dev/null +++ b/src/entities/interfaces/IUserProfile.ts @@ -0,0 +1,8 @@ +export interface IUserProfile { + id: number; + email: string; + lastName: string; + firstName: string; + phone: string; + isAdmin: boolean; +} diff --git a/src/features/LangSwitcher/LangSwitcher.sass b/src/features/LangSwitcher/LangSwitcher.sass new file mode 100644 index 000000000..75208d4b2 --- /dev/null +++ b/src/features/LangSwitcher/LangSwitcher.sass @@ -0,0 +1,6 @@ +.root + border: 0 + padding: 4px + background: none + outline: none + cursor: pointer \ No newline at end of file diff --git a/src/features/LangSwitcher/LangSwitcher.tsx b/src/features/LangSwitcher/LangSwitcher.tsx new file mode 100644 index 000000000..bc8aaac1f --- /dev/null +++ b/src/features/LangSwitcher/LangSwitcher.tsx @@ -0,0 +1,28 @@ +import React, { FC, useContext } from 'react'; +import cn from 'clsx'; +import { useTranslation } from 'react-i18next'; + +import s from './LangSwitcher.sass'; +import { Locale } from 'src/localization/settings'; +import { ThemeContext } from 'src/context/themeContext'; +import { Link } from 'react-router'; + +export type ThemeSwitcherProps = { + className?: string; +}; + +export const LangSwitcher: FC = ({ className }) => { + const { i18n } = useTranslation(); + const theme = useContext(ThemeContext); + + const lang = (i18n.language as Locale) === Locale.ru ? Locale.en : Locale.ru; + return ( + i18n.changeLanguage(lang)} + > + {lang} + + ); +}; diff --git a/src/features/store/cart.slice.ts b/src/features/store/cart.slice.ts new file mode 100644 index 000000000..98134404b --- /dev/null +++ b/src/features/store/cart.slice.ts @@ -0,0 +1,42 @@ +import { createSlice } from '@reduxjs/toolkit'; +import { loadState } from './storage'; +import { ICartProduct } from 'src/entities/interfaces/ICartProduct'; +import type {PayloadAction} from "@reduxjs/toolkit" +export const CART_PERSISTENT_STATE = 'cartData'; + +export interface ICartState { + items: ICartProduct[]; +} + +const initialState: ICartState = loadState(CART_PERSISTENT_STATE) ?? { + items: [], +}; + +const cartSlice = createSlice({ + name: 'cart', + initialState, + reducers: { + clear: (state) => { + state.items = []; + }, + delete: (state, action: PayloadAction) => { + state.items = state.items.filter((i) => i.id !== action.payload); + }, + add: (state, action: PayloadAction) => { + const existed = state.items.find((i) => i === action.payload); + // if (!existed) { + state.items.push(action.payload); + return; + // } + // state.items.map((i) => { + // if (i === action.payload) { + // i.count += 1; + // } + // return i; + // }); + }, + }, +}); + +export default cartSlice.reducer; +export const cartActions = cartSlice.actions; diff --git a/src/features/store/product.slice.ts b/src/features/store/product.slice.ts new file mode 100644 index 000000000..f587fb858 --- /dev/null +++ b/src/features/store/product.slice.ts @@ -0,0 +1,43 @@ +import { createSlice } from '@reduxjs/toolkit'; +import { loadState } from './storage'; +import IShortProduct from 'src/entities/interfaces/IShortProduct'; +import type {PayloadAction} from "@reduxjs/toolkit" + +export const PRODUCT_PERSISTENT_STATE = 'productData'; + +export interface IProductState { + items: IShortProduct[]; +} + +const initialState: IProductState = loadState(PRODUCT_PERSISTENT_STATE) ?? { + items: [], +}; + +const productSlice = createSlice({ + name: 'product', + initialState, + reducers: { + clear: (state) => { + state.items = []; + }, + delete: (state, action: PayloadAction) => { + state.items = state.items.filter((i) => i.id !== action.payload); + }, + add: (state, action: PayloadAction) => { + const existed = state.items.find((i) => i === action.payload); + // if (!existed) { + state.items.push(action.payload); + return; + // } + // state.items.map((i) => { + // if (i === action.payload) { + // i.count += 1; + // } + // return i; + // }); + }, + }, +}); + +export default productSlice.reducer; +export const productActions = productSlice.actions; diff --git a/src/features/store/storage.ts b/src/features/store/storage.ts new file mode 100644 index 000000000..7ff3db3a3 --- /dev/null +++ b/src/features/store/storage.ts @@ -0,0 +1,17 @@ +export function loadState(key: string): T | undefined { + try { + const jsonState = localStorage.getItem(key); + if (!jsonState) { + return undefined; + } + return JSON.parse(jsonState); + } catch (e) { + console.error(e); + return undefined; + } +} + +export function saveState(state: T, key: string) { + const stringState = JSON.stringify(state); + localStorage.setItem(key, stringState); +} diff --git a/src/features/store/store.ts b/src/features/store/store.ts new file mode 100644 index 000000000..ef507a2ea --- /dev/null +++ b/src/features/store/store.ts @@ -0,0 +1,37 @@ +import { combineReducers, configureStore } from '@reduxjs/toolkit'; +import storage from 'redux-persist/lib/storage'; +import userSlice, { JWT_PERSISTENT_STATE } from './user.slice'; +import cartSlice from './cart.slice'; +import productSlice from './product.slice'; +import persistReducer from 'redux-persist/es/persistReducer'; +import persistStore from 'redux-persist/es/persistStore'; + +const persistConfig = { + key: 'root', + storage, + debug: true, +}; + +const rootReducer = combineReducers({ userData: userSlice, cartData: cartSlice, productData: productSlice }); + +const persistedReducer = persistReducer(persistConfig, rootReducer); + +export const store = configureStore({ + reducer: { + user: persistedReducer, + }, + middleware: (getDefaultMiddleware) => + getDefaultMiddleware({ + serializableCheck: { + ignoredActions: ['persist/PERSIST', 'persist/REHYDRATE'], + }, + }), +}); + +// store.subscribe(() => { +// saveState({ userData: store.getState().user }, JWT_PERSISTENT_STATE); +// }); + +export type RootState = ReturnType; +export type AppDispath = typeof store.dispatch; +export const persistor = persistStore(store); diff --git a/src/features/store/user.slice.ts b/src/features/store/user.slice.ts new file mode 100644 index 000000000..b5a4f668a --- /dev/null +++ b/src/features/store/user.slice.ts @@ -0,0 +1,37 @@ +import { createSlice } from '@reduxjs/toolkit'; +import { loadState } from './storage'; +import { IUserProfile } from 'src/entities/interfaces/IUserProfile'; +import type {PayloadAction} from "@reduxjs/toolkit" + +export const JWT_PERSISTENT_STATE = 'userData'; + +export interface IUserState { + jwt: string | null; + profile?: IUserProfile; +} + +const initialState: IUserState = { + jwt: loadState(JWT_PERSISTENT_STATE)?.jwt ?? null, + profile: loadState(JWT_PERSISTENT_STATE)?.profile ?? null, +}; + +const userSlice = createSlice({ + name: 'user', + initialState, + reducers: { + addJwt: (state, action: PayloadAction) => { + state.jwt = action.payload; + }, + addProfile: (state, action: PayloadAction) => { + state.jwt = action.payload.jwt; + state.profile = action.payload.profile; + }, + logout: (state) => { + state.jwt = null; + state.profile = null; + }, + }, +}); + +export default userSlice.reducer; +export const userActions = userSlice.actions; diff --git a/src/homeworks/ts1/1_base.test.js b/src/homeworks/ts1/1_base.test.js index ee68dc16f..ff77416da 100644 --- a/src/homeworks/ts1/1_base.test.js +++ b/src/homeworks/ts1/1_base.test.js @@ -1,26 +1,26 @@ // Этот блок кода удалить и раскомментировать код ниже -it('remove it', () => { - expect(true).toBe(true); -}); - -// import { transformCustomers } from './1_base'; -// -// describe('all', () => { -// it('transformCustomers', () => { -// const customers = [ -// { id: 1, name: 'John', age: 25, isSubscribed: true }, -// { id: 2, name: 'Mary', age: 40, isSubscribed: false }, -// { id: 3, name: 'Bob', age: 32, isSubscribed: true }, -// { id: 4, name: 'Alice', age: 22, isSubscribed: true }, -// { id: 5, name: 'David', age: 48, isSubscribed: false }, -// ]; -// -// expect(transformCustomers(customers)).toEqual({ -// 1: { name: 'John', age: 25, isSubscribed: true }, -// 2: { name: 'Mary', age: 40, isSubscribed: false }, -// 3: { name: 'Bob', age: 32, isSubscribed: true }, -// 4: { name: 'Alice', age: 22, isSubscribed: true }, -// 5: { name: 'David', age: 48, isSubscribed: false }, -// }); -// }); +// it('remove it', () => { +// expect(true).toBe(true); // }); + +import { transformCustomers } from './1_base'; + +describe('all', () => { + it('transformCustomers', () => { + const customers = [ + { id: 1, name: 'John', age: 25, isSubscribed: true }, + { id: 2, name: 'Mary', age: 40, isSubscribed: false }, + { id: 3, name: 'Bob', age: 32, isSubscribed: true }, + { id: 4, name: 'Alice', age: 22, isSubscribed: true }, + { id: 5, name: 'David', age: 48, isSubscribed: false }, + ]; + + expect(transformCustomers(customers)).toEqual({ + 1: { name: 'John', age: 25, isSubscribed: true }, + 2: { name: 'Mary', age: 40, isSubscribed: false }, + 3: { name: 'Bob', age: 32, isSubscribed: true }, + 4: { name: 'Alice', age: 22, isSubscribed: true }, + 5: { name: 'David', age: 48, isSubscribed: false }, + }); + }); +}); diff --git a/src/homeworks/ts1/1_base.js b/src/homeworks/ts1/1_base.ts similarity index 53% rename from src/homeworks/ts1/1_base.js rename to src/homeworks/ts1/1_base.ts index 611b3a92f..2bc11f275 100644 --- a/src/homeworks/ts1/1_base.js +++ b/src/homeworks/ts1/1_base.ts @@ -1,16 +1,16 @@ /** * Нужно превратить файл в ts и указать типы аргументов и типы возвращаемого значения * */ -export const removePlus = (string) => string.replace(/^\+/, ''); +export const removePlus = (string: string): string => string.replace(/^\+/, ''); -export const addPlus = (string) => `+${string}`; +export const addPlus = (string: string): string => `+${string}`; -export const removeFirstZeros = (value) => value.replace(/^(-)?[0]+(-?\d+.*)$/, '$1$2'); +export const removeFirstZeros = (value: string): string => value.replace(/^(-)?[0]+(-?\d+.*)$/, '$1$2'); -export const getBeautifulNumber = (value, separator = ' ') => +export const getBeautifulNumber = (value: number, separator = ' '): string => value?.toString().replace(/\B(?=(\d{3})+(?!\d))/g, separator); -export const round = (value, accuracy = 2) => { +export const round = (value: number, accuracy = 2): number => { const d = 10 ** accuracy; return Math.round(value * d) / d; }; @@ -18,7 +18,8 @@ export const round = (value, accuracy = 2) => { const transformRegexp = /(matrix\(-?\d+(\.\d+)?, -?\d+(\.\d+)?, -?\d+(\.\d+)?, -?\d+(\.\d+)?, )(-?\d+(\.\d+)?), (-?\d+(\.\d+)?)\)/; -export const getTransformFromCss = (transformCssString) => { +type coordinate = { x: number; y: number }; +export const getTransformFromCss = (transformCssString: string): coordinate => { const data = transformCssString.match(transformRegexp); if (!data) return { x: 0, y: 0 }; return { @@ -27,20 +28,20 @@ export const getTransformFromCss = (transformCssString) => { }; }; -export const getColorContrastValue = ([red, green, blue]) => +export const getColorContrastValue = ([red, green, blue]: [number, number, number]): number => // http://www.w3.org/TR/AERT#color-contrast Math.round((red * 299 + green * 587 + blue * 114) / 1000); -export const getContrastType = (contrastValue) => (contrastValue > 125 ? 'black' : 'white'); +export const getContrastType = (contrastValue: number): string => (contrastValue > 125 ? 'black' : 'white'); export const shortColorRegExp = /^#[0-9a-f]{3}$/i; export const longColorRegExp = /^#[0-9a-f]{6}$/i; -export const checkColor = (color) => { +export const checkColor = (color: string): void | never => { if (!longColorRegExp.test(color) && !shortColorRegExp.test(color)) throw new Error(`invalid hex color: ${color}`); }; -export const hex2rgb = (color) => { +export const hex2rgb = (color: string): [number, number, number] => { checkColor(color); if (shortColorRegExp.test(color)) { const red = parseInt(color.substring(1, 2), 16); @@ -54,12 +55,24 @@ export const hex2rgb = (color) => { return [red, green, blue]; }; -export const getNumberedArray = (arr) => arr.map((value, number) => ({ value, number })); -export const toStringArray = (arr) => arr.map(({ value, number }) => `${value}_${number}`); +export const getNumberedArray = (arr: [T]): { value: T; number: number }[] => + arr.map((value, number) => ({ value, number })); +export const toStringArray = (arr: [{ value: T; number: number }]) => + arr.map(({ value, number }) => `${value}_${number}`); -export const transformCustomers = (customers) => { +type Key = { + id: number; +}; +type Customer = { + name: string; + age: number; + isSubscribed: boolean; +}; +type CustomerKey = Key & Customer; + +export const transformCustomers = (customers: CustomerKey[]) => { return customers.reduce((acc, customer) => { acc[customer.id] = { name: customer.name, age: customer.age, isSubscribed: customer.isSubscribed }; return acc; - }, {}); + }, {} as Customer[]); }; diff --git a/src/homeworks/ts1/2_repair.ts b/src/homeworks/ts1/2_repair.ts index 19e98c068..86ead12eb 100644 --- a/src/homeworks/ts1/2_repair.ts +++ b/src/homeworks/ts1/2_repair.ts @@ -2,46 +2,50 @@ * Здесь код с ошибками типов. Нужно их устранить * */ -// // Мы это не проходили, но по тексту ошибки можно понять, как это починить -// export const getFakeApi = async (): void => { -// const result = await fetch('https://jsonplaceholder.typicode.com/todos/1').then((response) => response.json()); -// console.log(result); -// }; -// -// // Мы это не проходили, но по тексту ошибки можно понять, как это починить -// export class SomeClass { -// constructor() { -// this.set = new Set([1]); -// this.channel = new BroadcastChannel('test-broadcast-channel'); -// } -// } -// -// export type Data = { -// type: 'Money' | 'Percent'; -// value: DataValue; -// }; -// -// export type DataValue = Money | Percent; -// -// export type Money = { -// currency: string; -// amount: number; -// }; -// -// export type Percent = { -// percent: number; -// }; -// -// // Здесь, возможно, нужно использовать as, возможно в switch передавать немного по-другому -// const getDataAmount = (data: Data): number => { -// switch (data.type) { -// case 'Money': -// return data.value.amount; -// -// default: { -// // eslint-disable-next-line @typescript-eslint/no-unused-vars -// const unhandled: never = data; // здесь, возможно, нужно использовать нечто другое. :never должен остаться -// throw new Error(`unknown type: ${data.type}`); -// } -// } -// }; +// Мы это не проходили, но по тексту ошибки можно понять, как это починить +export const getFakeApi = async (): Promise => { + const result = await fetch('https://jsonplaceholder.typicode.com/todos/1').then((response) => response.json()); + console.log(result); +}; + +// Мы это не проходили, но по тексту ошибки можно понять, как это починить +export class SomeClass { + set: Set; + channel: BroadcastChannel; + + constructor() { + this.set = new Set([1]); + this.channel = new BroadcastChannel('test-broadcast-channel'); + } +} + +export type Data = { + type: 'Money' | 'Percent'; + value: DataValue; +}; + +export type DataValue = Money | Percent; + +export type Money = { + currency: string; + amount: number; +}; + +export type Percent = { + percent: number; +}; + +// Здесь, возможно, нужно использовать as, возможно в switch передавать немного по-другому +const getDataAmount = (data: Data): number => { + switch (data.type) { + case 'Money': + return (data.value as Money).amount; + case 'Percent': + return (data.value as Percent).percent; + default: { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const unhandled: never | Data = data; // здесь, возможно, нужно использовать нечто другое. :never должен остаться + throw new Error(`unknown type: ${data.type}`); + } + } +}; diff --git a/src/homeworks/ts1/3_write.test.js b/src/homeworks/ts1/3_write.test.js index 5b5173063..5a50badc4 100644 --- a/src/homeworks/ts1/3_write.test.js +++ b/src/homeworks/ts1/3_write.test.js @@ -1,35 +1,35 @@ // Этот блок кода удалить и раскомментировать код ниже -it('remove it', () => { - expect(true).toBe(true); -}); - -// import { createRandomOperation, createRandomProduct } from './3_write'; -// -// describe('all', () => { -// it('operation', () => { -// const createdAt = '2023-06-06T12:06:56.957Z'; -// const operation = createRandomOperation(createdAt); -// expect(operation).toHaveProperty('createdAt', createdAt); -// expect(operation).toHaveProperty('id'); -// expect(operation).toHaveProperty('name'); -// expect(operation).toHaveProperty('desc'); -// expect(operation).toHaveProperty('createdAt'); -// expect(operation).toHaveProperty('amount'); -// expect(operation).toHaveProperty('category'); -// expect(operation).toHaveProperty('type'); -// }); -// -// it('product', () => { -// const createdAt = '2023-06-06T12:06:56.957Z'; -// const product = createRandomProduct(createdAt); -// expect(product).toHaveProperty('createdAt', createdAt); -// expect(product).toHaveProperty('id'); -// expect(product).toHaveProperty('name'); -// expect(product).toHaveProperty('photo'); -// expect(product).toHaveProperty('desc'); -// expect(product).toHaveProperty('createdAt'); -// expect(product).toHaveProperty('oldPrice'); -// expect(product).toHaveProperty('price'); -// expect(product).toHaveProperty('category'); -// }); +// it('remove it', () => { +// expect(true).toBe(true); // }); + +import { createRandomOperation, createRandomProduct } from './3_write'; + +describe('all', () => { + it('operation', () => { + const createdAt = '2023-06-06T12:06:56.957Z'; + const operation = createRandomOperation(createdAt); + expect(operation).toHaveProperty('createdAt', createdAt); + expect(operation).toHaveProperty('id'); + expect(operation).toHaveProperty('name'); + expect(operation).toHaveProperty('desc'); + expect(operation).toHaveProperty('createdAt'); + expect(operation).toHaveProperty('amount'); + expect(operation).toHaveProperty('category'); + expect(operation).toHaveProperty('type'); + }); + + it('product', () => { + const createdAt = '2023-06-06T12:06:56.957Z'; + const product = createRandomProduct(createdAt); + expect(product).toHaveProperty('createdAt', createdAt); + expect(product).toHaveProperty('id'); + expect(product).toHaveProperty('name'); + expect(product).toHaveProperty('photo'); + expect(product).toHaveProperty('desc'); + expect(product).toHaveProperty('createdAt'); + expect(product).toHaveProperty('oldPrice'); + expect(product).toHaveProperty('price'); + expect(product).toHaveProperty('category'); + }); +}); diff --git a/src/homeworks/ts1/3_write.ts b/src/homeworks/ts1/3_write.ts index 15f9dcdf2..f237ef636 100644 --- a/src/homeworks/ts1/3_write.ts +++ b/src/homeworks/ts1/3_write.ts @@ -1,3 +1,4 @@ +import { faker } from '@faker-js/faker'; /** * Функции написанные здесь пригодятся на последующих уроках * С помощью этих функций мы будем добавлять элементы в список для проверки динамической загрузки @@ -42,15 +43,75 @@ * - category (Категория) * - type ('Profit') * */ +export type Category = { + id: number; + name: string; + photo?: string; +}; +export type Product = { + id: number; + name: string; + photo: string; + desc?: string; + createdAt: string; + oldPrice?: number; + price: number; + category: Category; +}; + +export type Cost = { + id: string; + name: string; + desc?: string; + createdAt: string; + amount: number; + category: Category; + type: Cost; +}; + +export type Profit = { + id: string; + name: string; + desc?: string; + createdAt: string; + amount: number; + category: Category; + type: Profit; +}; /** * Создает случайный продукт (Product). * Принимает дату создания (строка) * */ -// export const createRandomProduct = (createdAt: string) => {}; +export const createRandomProduct = (createdAt: string): Product => ({ + id: faker.number.int({ max: 5 }), + name: faker.commerce.product(), + photo: faker.image.avatar(), + desc: faker.commerce.productDescription(), + createdAt: createdAt, + oldPrice: +faker.commerce.price({ min: 1000, max: 5000 }), + price: +faker.commerce.price({ min: 1000, max: 5000 }), + category: { + id: faker.number.int(), + name: faker.company.name(), + photo: faker.image.avatar(), + }, +}); /** * Создает случайную операцию (Operation). * Принимает дату создания (строка) * */ -// export const createRandomOperation = (createdAt: string) => {}; +export const createRandomOperation = (createdAt: string): Cost | Profit => ({ + id: faker.string.sample(), + createdAt: createdAt, + name: faker.person.fullName(), + desc: 'desc', + amount: faker.number.int(), + category: { + id: faker.number.int(), + name: faker.company.name(), + photo: faker.image.avatar(), + }, + type: this, +}); diff --git a/src/homeworks/ts1/accountService/AccountService.test.ts b/src/homeworks/ts1/accountService/AccountService.test.ts new file mode 100644 index 000000000..0b779c193 --- /dev/null +++ b/src/homeworks/ts1/accountService/AccountService.test.ts @@ -0,0 +1,53 @@ +import { AccountService } from './AccountService'; +import { productDiscount, userDiscount } from './accountServiceEnum'; + +describe('AccountService', () => { + let accountService: AccountService; + + beforeEach(() => { + accountService = new AccountService(); + }); + + it('Set user discount', () => { + accountService.setUserDiscount(userDiscount.Premium, 30); + accountService.setUserDiscount(userDiscount.Standard, 20); + + const discountPremium = accountService.getDiscount(productDiscount.Car, userDiscount.Premium); + const discountStandard = accountService.getDiscount(productDiscount.Car, userDiscount.Standard); + + expect(discountPremium).toBe(30); + expect(discountStandard).toBe(20); + }); + + it('Set product and user discount', () => { + accountService.setUserDiscount(userDiscount.Premium, 30); + accountService.setUserDiscount(userDiscount.Standard, 20); + accountService.setUserDiscount(userDiscount.Gold, 30); + + accountService.setProductDiscount(productDiscount.Car, userDiscount.Premium, 10); + accountService.setProductDiscount(productDiscount.Car, userDiscount.Standard, 5); + + const discountPremium = accountService.getDiscount(productDiscount.Car, userDiscount.Premium); + const discountStandard = accountService.getDiscount(productDiscount.Car, userDiscount.Standard); + const discountFree = accountService.getDiscount(productDiscount.Car, userDiscount.Free); + const discountGold = accountService.getDiscount(productDiscount.Car, userDiscount.Gold); + + expect(discountPremium).toBe(40); + expect(discountStandard).toBe(25); + expect(discountGold).toBe(30); + expect(discountFree).toBe(0); + + }); + + it('Set user negative discount', () => { + expect(() => accountService.setUserDiscount(userDiscount.Premium, -15)).toThrow( + 'Скидка не может быть меньше нуля!' + ); + }); + + it('Set product negative discount', () => { + expect(() => accountService.setProductDiscount(productDiscount.Food, userDiscount.Premium, -10)).toThrow( + 'Скидка не может быть меньше нуля!' + ); + }); +}); diff --git a/src/homeworks/ts1/accountService/AccountService.ts b/src/homeworks/ts1/accountService/AccountService.ts new file mode 100644 index 000000000..8dac1f00f --- /dev/null +++ b/src/homeworks/ts1/accountService/AccountService.ts @@ -0,0 +1,35 @@ +import { productDiscount, userDiscount } from './accountServiceEnum'; + +export type UserType = userDiscount; +export type ProductType = productDiscount; + +export class AccountService { + private userDiscounts: Map = new Map(); + private productDiscounts: Map> = new Map(); + + setUserDiscount(user: UserType, discount: number) { + if (discount < 0) throw new Error('Скидка не может быть меньше нуля!'); + + this.userDiscounts.set(user, discount); + } + + setProductDiscount(productType: ProductType, userType: UserType, discount: number) { + if (discount < 0) { + throw new Error('Скидка не может быть меньше нуля!'); + } + + if (!this.productDiscounts.has(productType)) { + this.productDiscounts.set(productType, new Map()); + } + + const mapProducts = this.productDiscounts.get(productType); + mapProducts.set(userType, discount); + } + + getDiscount(productType: ProductType, userType: UserType): number { + const userDiscount = this.userDiscounts.get(userType) ?? 0; + const productDiscount = this.productDiscounts.get(productType)?.get(userType) ?? 0; + + return userDiscount + productDiscount; + } +} diff --git a/src/homeworks/ts1/accountService/accountServiceEnum.ts b/src/homeworks/ts1/accountService/accountServiceEnum.ts new file mode 100644 index 000000000..c2180be0a --- /dev/null +++ b/src/homeworks/ts1/accountService/accountServiceEnum.ts @@ -0,0 +1,12 @@ +export enum userDiscount { + Standard = 'Standard', + Premium = 'Premium', + Gold = 'Gold', + Free = 'Free', +} + +export enum productDiscount { + Car = 'Car', + Toy = 'Toy', + Food = 'Food', +} diff --git a/src/app/index.css b/src/index.css similarity index 100% rename from src/app/index.css rename to src/index.css diff --git a/src/index.html b/src/index.html index c8a8e9634..372160e9f 100644 --- a/src/index.html +++ b/src/index.html @@ -7,5 +7,6 @@
+
diff --git a/src/index.tsx b/src/index.tsx index 26d2b1437..ab667e4df 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,11 +1,23 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; -import './app/index.css'; +import './index.css'; import App from './app/App'; +import 'bootstrap/dist/css/bootstrap.css'; +import 'bootstrap/dist/js/bootstrap.min.js'; +import { persistor, store } from './features/store/store'; +import { Provider } from 'react-redux'; +import { PersistGate } from 'redux-persist/integration/react'; +import { BrowserRouter } from 'react-router'; const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); root.render( - + + + + + + + ); diff --git a/src/localization/LocalizationInitiator.tsx b/src/localization/LocalizationInitiator.tsx new file mode 100644 index 000000000..b0665a2f0 --- /dev/null +++ b/src/localization/LocalizationInitiator.tsx @@ -0,0 +1,15 @@ +import { FC, useInsertionEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { LANG_STORAGE_KEY } from './settings'; + +export const LocalizationInitiator: FC = () => { + const { i18n } = useTranslation(); + + useInsertionEffect(() => { + localStorage.setItem(LANG_STORAGE_KEY, i18n.language); + const html = document.body.parentElement; + html.lang = i18n.language; + }, [i18n.language]); + + return null; +}; diff --git a/src/localization/resources.ts b/src/localization/resources.ts new file mode 100644 index 000000000..2d34fc45e --- /dev/null +++ b/src/localization/resources.ts @@ -0,0 +1,362 @@ +// the translations +// (tip move them in a JSON file and import them, +// or even better, manage them separated from your code: https://react.i18next.com/guides/multiple-translation-files) +export const resources = { + en: { + translation: { + errors: { + unexpected_error: 'Unexpected error. We automatically register errors and will fix everything soon', + 'Failed to fetch': 'Connection error. Go to the server directory and start the server using the start script', + is_required: 'Required field', + invalid_email_address: 'Invalid email address', + too_short_password: 'The password is too short', + not_same_password: "Passwords don't match", + ERR_INCORRECT_EMAIL_OR_PASSWORD: 'Incorrect password or email', + ERR_NOT_FOUND: 'An entity with this id was not found', + ERR_USER_NOT_REGISTER: 'Register to access this feature', + ERR_INCORRECT_PASSWORD: 'Invalid password', + ERR_ACCOUNT_ALREADY_EXIST: 'An account with this email already exists', + ERR_INVALID_PASSWORD: 'The password must contain at least 8 characters', + ERR_INVALID_EMAIL: 'Invalid email', + ERR_TOKEN_REQUIRED_ERROR: + 'Server token error. We automatically register all errors and will fix everything soon', + ERR_AUTH_ERROR: 'You are not logged in, log in to your account and try again', + ERR_DATA_BASE_ERROR: 'Database server error. We automatically register all errors and will fix everything soon', + INTERNAL_SERVER_ERROR: 'Server error. We automatically register all errors and will fix everything soon', + ERR_INVALID_NICKNAME: + 'The alias must be at least 7 characters and can contain only numbers, letters and an underscore', + }, + customText: { + desc: 'An import substitution company needs to rewrite a project from Blazor to React.', + }, + screens: { + ProfileScreen: { + title: 'Profile', + updateProfile: { + title: 'Change profile', + success: 'Profile changed successfully', + save: 'Save', + }, + changePassword: { + title: 'Change password', + success: 'Password changed successfully', + save: 'Change', + }, + }, + ExamplesScreen: { + title: 'Examples', + }, + LessonsScreen: { + title: 'Lessons', + }, + auth: { + title: 'Authentication', + signIn: { + title: 'Sign in', + submit: 'Sign in', + }, + signUp: { + title: 'Sign out', + submit: 'Sign out', + }, + }, + HomeScreen: { + title: 'Home', + desc: `Welcome to the react course from otus!\n\nThis is a training project of the course. Here you will find materials for lectures, homework, as well as examples of some complex components.\n\nThe project is written in **typescript**, **redux**, **redux-saga** using **webpack**. In it , you can spy on the implementation of the dark/light theme, locale switching, navigation, token exchange between browser tabs.\n\nThe project also has its own server implemented on **graphql**, **apollo**, **express** and using a json file as a database (in real development, do not do this, it's just to save you from installing the database). Interaction with the server is implemented, namely the ability to register a user, log in to an account, but do not forget to start the server locally - use the _start:client_ and _start:server_ commands.\n\nWelcome and good luck in learning!`, + }, + TeachersScreen: { + title: 'Teachers', + desc: "Someday there will be teachers' cards here, but that's not for sure", + }, + }, + forms: { + AuthForm: { + email: { + title: 'Email', + placeholder: 'Enter email', + }, + password: { + title: 'Password', + placeholder: 'Enter password', + }, + }, + ChangePasswordForm: { + password: { + title: 'Password', + placeholder: 'Enter password', + }, + newPassword: { + title: 'New password', + placeholder: 'Enter new password', + }, + repeatPassword: { + title: 'Repeat password', + placeholder: 'Repeat password', + }, + }, + EmailForm: { + email: { + title: 'Email', + placeholder: 'Enter email', + }, + }, + ProfileForm: { + name: { + title: 'Nickname', + placeholder: 'Come up with a pseudonym for yourself', + }, + about: { + title: 'About', + placeholder: 'Write something about yourself', + }, + }, + RepeatPasswordForm: { + password: { + title: 'Password', + placeholder: 'Enter password', + }, + repeatPassword: { + title: 'Repeat password', + placeholder: 'Repeat password', + }, + }, + }, + fields: { + string: 'String value', + }, + components: { + RemoveButton: { + title: 'Data will be lost, delete?', + ok: 'Remove', + cancel: 'Cancel', + }, + RangeInputs: { + from: 'From', + to: 'To', + }, + InputIntRangeList: { + title: 'Range', + }, + NumberInput: { + float: { + placeholder: 'Fractional number', + }, + integer: { + placeholder: 'Integer', + }, + }, + login: { + enter: 'Login', + leave: 'Logout', + }, + header: { + nav: 'Navigation', + root: 'Home', + profile: 'Profile', + 'home-works': 'Home works', + examples: 'Examples', + lessons: 'Lessons', + teachers: 'Teachers', + }, + }, + enums: { + ExampleKey: { + modal: 'Modal', + movable: 'Movable', + sortableList: 'Sortable List', + waveSlider: 'Wave Slider', + inputs: 'Inputs', + }, + LessonKey: { + generator: 'Generators iterators', + patterns: 'Patterns', + solid: 'Solid', + functionalProgramming: 'Functional Programming', + babel: 'Babel + typescript', + socketsAndWorkers: 'Web sockets & Web workers', + graphql: 'Graphql', + }, + }, + }, + }, + ru: { + translation: { + errors: { + unexpected_error: 'Неожиданная ошибка. Мы автоматически регистрируем ошибки и скоро все исправим', + 'Failed to fetch': + 'Ошибка соединения. Перейдите в директорию server и запустите сервер с помощью скрипта start', + is_required: 'Обязательное поле', + invalid_email_address: 'Некорректный email адрес', + too_short_password: 'Слишком короткий пароль', + not_same_password: 'Пароли не совпадают', + ERR_INCORRECT_EMAIL_OR_PASSWORD: 'Некорректный пароль или email', + ERR_NOT_FOUND: 'Сущность с таким id не найдена', + ERR_USER_NOT_REGISTER: 'Зарегистрируйтесь, чтобы получить доступ к этой функции', + ERR_INCORRECT_PASSWORD: 'Некорректный пароль', + ERR_ACCOUNT_ALREADY_EXIST: 'Аккаунт с таким email уже существует', + ERR_INVALID_PASSWORD: 'Пароль должен содержать от 8 символов', + ERR_INVALID_EMAIL: 'Некорректный email', + ERR_TOKEN_REQUIRED_ERROR: + 'Серверная ошибка токена. Мы автоматически регистрируем все ошибки и скоро все исправим', + ERR_AUTH_ERROR: 'Вы не авторизованы, войдите в учетную запись и повторите попытку', + ERR_DATA_BASE_ERROR: + 'Серверная ошибка базы данный. Мы автоматически регистрируем все ошибки и скоро все исправим', + INTERNAL_SERVER_ERROR: 'Серверная ошибка. Мы автоматически регистрируем все ошибки и скоро все исправим', + ERR_INVALID_NICKNAME: + 'Псевдоним должен быть от 7 символов и может содержать только числа, буквы и символ нижнего подчеркивания', + }, + customText: { + desc: 'An import substitution company needs to rewrite a project from Blazor to React.', + }, + screens: { + ProfileScreen: { + title: 'Профиль', + updateProfile: { + title: 'Изменить профиль', + success: 'Профиль успешно изменен', + save: 'Сохранить', + }, + changePassword: { + title: 'Изменить пароль', + success: 'Пароль успешно изменен', + save: 'Изменить', + }, + }, + ExamplesScreen: { + title: 'Примеры', + }, + LessonsScreen: { + title: 'Уроки', + }, + auth: { + title: 'Аутентификация', + signIn: { + title: 'Войти', + submit: 'Войти', + }, + signUp: { + title: 'Зарегистрироваться', + submit: 'Зарегистрироваться', + }, + }, + HomeScreen: { + title: 'Главная', + desc: `Приветствую на курсе по react от otus!\n\nЭто учебный проект курса. Здесь вы найдете материалы к лекциям, домашние задания, а так же примеры некоторых сложных компонентов.\n\nПроект написан на **typescript**, **redux**, **redux-saga** с использованием **webpack**. В нем можно подсмотреть реализацию темной/светлой темы, переключение локали, навигацию, обмен токен между вкладками браузера.\n\nТак же в проекте есть свой сервер, реализованный на **graphql**, **apollo**, **express** и использующий в качестве базы данных json файл (в реальной разработке не делайте так, это только чтобы избавить вас от установки базы данных). Реализовано взаимодействие с сервером, а именно возможность зарегистрировать пользователя, войти в учетную запись, но не забудьте запустить сервер локально - используйте команды _start:client_ и _start:server_.\n\nДобро пожаловать и удачи в обучении!`, + }, + TeachersScreen: { + title: 'Преподаватели', + desc: 'Когда-нибудь здесь появятся карточки преподавателей, но это не точно', + }, + }, + forms: { + AuthForm: { + email: { + title: 'Email', + placeholder: 'Укажите email', + }, + password: { + title: 'Пароль', + placeholder: 'Укажите пароль', + }, + }, + ChangePasswordForm: { + password: { + title: 'Пароль', + placeholder: 'Укажите пароль', + }, + newPassword: { + title: 'Новый пароль', + placeholder: 'Укажите новый пароль', + }, + repeatPassword: { + title: 'Повторите пароль', + placeholder: 'Повторите пароль', + }, + }, + EmailForm: { + email: { + title: 'Email', + placeholder: 'Укажите email', + }, + }, + ProfileForm: { + name: { + title: 'Псевдоним', + placeholder: 'Придумайте себе псевдоним', + }, + about: { + title: 'О себе', + placeholder: 'Напишите что-нибудь о себе', + }, + }, + RepeatPasswordForm: { + password: { + title: 'Пароль', + placeholder: 'Укажите пароль', + }, + repeatPassword: { + title: 'Повторите пароль', + placeholder: 'Повторите пароль', + }, + }, + }, + fields: { + string: 'Строковое значение', + }, + components: { + RemoveButton: { + title: 'Данные будут потеряны, удалить?', + ok: 'Удалить', + cancel: 'Отмена', + }, + RangeInputs: { + from: 'От', + to: 'До', + }, + InputIntRangeList: { + title: 'Диапазон', + }, + NumberInput: { + float: { + placeholder: 'Дробное число', + }, + integer: { + placeholder: 'Целое число', + }, + }, + login: { + enter: 'Вход', + leave: 'Выход', + }, + header: { + nav: 'Навигация', + root: 'Главная', + profile: 'Профиль', + 'home-works': 'Домашние работы', + examples: 'Примеры', + lessons: 'Уроки', + teachers: 'Преподаватели', + }, + }, + enums: { + ExampleKey: { + modal: 'Модальное окно', + movable: 'Перемещаемый', + sortableList: 'Сортируемый список', + waveSlider: 'Слайдер', + inputs: 'Инпуты', + }, + LessonKey: { + generator: 'Генераторы итераторы', + patterns: 'Паттерны', + restApi: 'Rest Api', + solid: 'Solid', + functionalProgramming: 'Функциональное программирование', + babel: 'Babel + typescript', + socketsAndWorkers: 'Web sockets & Web workers', + graphql: 'Graphql', + }, + }, + }, + }, +}; diff --git a/src/localization/settings.ts b/src/localization/settings.ts new file mode 100644 index 000000000..d30c07b46 --- /dev/null +++ b/src/localization/settings.ts @@ -0,0 +1,28 @@ +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; +import { translation } from './translation'; +import LanguageDetector from 'i18next-browser-languagedetector'; +export const LANG_STORAGE_KEY = 'lang'; + +export enum Locale { + ru = 'ru', + en = 'en', +} + +i18n + .use(initReactI18next) // passes i18n down to react-i18next + .use(LanguageDetector) + .init({ + resources: translation, + // translation, + lng: localStorage.getItem(LANG_STORAGE_KEY) || Locale.ru, + // // language to use, more information here: https://www.i18next.com/overview/configuration-options#languages-namespaces-resources + // // you can use the i18n.changeLanguage function to change the language manually: https://www.i18next.com/overview/api#changelanguage + // // if you're using a language detector, do not define the lng option + + // interpolation: { + // escapeValue: false, // react already safes from xss + // }, + }); + +export default i18n; diff --git a/src/localization/translation.ts b/src/localization/translation.ts new file mode 100644 index 000000000..5f8842eb3 --- /dev/null +++ b/src/localization/translation.ts @@ -0,0 +1,20 @@ +export const translation = { + ru: { + translation: { + key1: 'В компании по импортозамещению необходимо переписать проект с Blazor на React.', + key2: 'Владею Blazor, AspNet, net.6', + key3: 'FullStack разработчик, разрабатываю информационно-аналитическую систему.', + key4: '2022-2023гг проходил обучение по курсу AspNet core Otus.', + teststorybook: 'Привет мир!', + }, + }, + en: { + translation: { + key1: 'An import substitution company needs to rewrite a project from Blazor to React.', + key2: 'I speak Blazor, AspNet, net.6', + key3: 'FullStack developer, developing an information and analytical system.', + key4: '2022-2023 studied at the AspNet core Otus course.', + teststorybook: 'Hello world!', + }, + }, +}; diff --git a/src/pages/AuthScreen/SingInBlock/Login.stories.ts b/src/pages/AuthScreen/SingInBlock/Login.stories.ts new file mode 100644 index 000000000..c0619f2c2 --- /dev/null +++ b/src/pages/AuthScreen/SingInBlock/Login.stories.ts @@ -0,0 +1,13 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import Login from './Login'; + +const meta: Meta = { + title: 'Homework2/Login', + component: Login, + tags: ['autodocs'], +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/src/pages/AuthScreen/SingInBlock/Login.tsx b/src/pages/AuthScreen/SingInBlock/Login.tsx new file mode 100644 index 000000000..b6ea6a303 --- /dev/null +++ b/src/pages/AuthScreen/SingInBlock/Login.tsx @@ -0,0 +1,101 @@ +import React, { useEffect } from 'react'; +import { useForm, SubmitHandler } from 'react-hook-form'; +import cn from 'clsx'; +import { useDispatch, useSelector } from 'react-redux'; +import { AppDispath, RootState } from 'src/features/store/store'; +import { IUserState, userActions } from 'src/features/store/user.slice'; +import { useNavigate } from 'react-router'; +import { IUserProfile } from 'src/entities/interfaces/IUserProfile'; + +interface IFormLogin { + email: string; + password: string; +} + +function Login() { + const { + register, + handleSubmit, + formState: { errors }, + } = useForm({ + defaultValues: { + email: 'test@mail.ru', + password: 'qazQ1@', + }, + mode: 'onChange', + }); + const dispatch = useDispatch(); + const navigate = useNavigate(); + const jwt = useSelector((s: RootState) => s.user.userData?.jwt); + const [checked, setChecked] = React.useState(false); + let profile: IUserProfile = { + id: 123, + lastName: 'Ivanov', + firstName: 'Ivan', + phone: '89260010101', + email: 'test@mail.ru', + isAdmin: false, + }; + + const handleChange = () => { + setChecked(!checked); + }; + + const onSubmit: SubmitHandler = (data) => { + const jwt = Math.random().toString(16); + const user: IUserState = { jwt: jwt, profile: { ...profile, email: data.email, isAdmin: checked } }; + dispatch(userActions.addProfile(user)); + navigate('/'); + }; + + const passwordError = ( +
    +
  • Минимальная длина пароля 5 символов
  • +
  • Пароль должен содержать хотя бы одну цифру
  • +
  • Пароль должен содержать хотя бы одну латинскую букву в нижнем регистре
  • +
  • Пароль должен содержать хотя бы одну латинскую букву в верхнем регистре
  • +
+ ); + + return ( +
+
+ + +
+
+ + +
{errors.password && passwordError}
+
+
+ + +
+ +
+ ); +} + +export default Login; diff --git a/src/pages/ListProduct/ListProductPage.tsx b/src/pages/ListProduct/ListProductPage.tsx new file mode 100644 index 000000000..06e675d21 --- /dev/null +++ b/src/pages/ListProduct/ListProductPage.tsx @@ -0,0 +1,25 @@ +import React, { useEffect, useState } from 'react'; +import { ListProduct } from 'src/shared/ui/listProduct'; +import { useInView } from 'react-intersection-observer'; +import { useDispatch, useSelector } from 'react-redux'; +import { AppDispath, RootState } from 'src/features/store/store'; +import { productActions } from 'src/features/store/product.slice'; + +const ListProductPage = () => { + const { ref, inView } = useInView({ threshold: 0.7 }); + + const products = useSelector((s: RootState) => s.user.productData.items); + const dispatch = useDispatch(); + + + return ( + <> + +
+ +
+ + ); +}; + +export default ListProductPage; diff --git a/src/pages/error/Error.tsx b/src/pages/error/Error.tsx new file mode 100644 index 000000000..eb1128a86 --- /dev/null +++ b/src/pages/error/Error.tsx @@ -0,0 +1,4 @@ +import React from 'react'; +export default function Error() { + return
Error
; +} diff --git a/src/pages/profile/UserProfile.tsx b/src/pages/profile/UserProfile.tsx new file mode 100644 index 000000000..7839bb891 --- /dev/null +++ b/src/pages/profile/UserProfile.tsx @@ -0,0 +1,59 @@ +import React from 'react'; +import { useSelector } from 'react-redux'; +import { RootState } from 'src/features/store/store'; + +const UserProfile = () => { + const profile = useSelector((s: RootState) => s.user.userData); + + return ( +
+
+
+
+ + + + +
+
+
+
+
+

Профиль

+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+ +
+
+
+
+
+ ); +}; +export default UserProfile; diff --git a/src/routes/helpers/ProtectedRouteAdmin.tsx b/src/routes/helpers/ProtectedRouteAdmin.tsx new file mode 100644 index 000000000..f0ce79188 --- /dev/null +++ b/src/routes/helpers/ProtectedRouteAdmin.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { useSelector } from 'react-redux'; +import { Navigate } from 'react-router'; +import { RootState } from 'src/features/store/store'; + +interface IProtectedRouteAdmin { + children: React.ReactElement; +} + +export const ProtectedRouteAdmin = ({ children }: IProtectedRouteAdmin) => { + const user = useSelector((s: RootState) => s.user); + + const isAuthenticated = user.userData?.jwt; + const isAdmin = user.userData?.profile?.isAdmin; + + if (!isAuthenticated || !isAdmin) { + return ; + } + + return children; +}; + diff --git a/src/routes/helpers/RequireAuth.tsx b/src/routes/helpers/RequireAuth.tsx new file mode 100644 index 000000000..41d95af93 --- /dev/null +++ b/src/routes/helpers/RequireAuth.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { useSelector } from 'react-redux'; +import { RootState } from '../../features/store/store'; +import { Navigate } from 'react-router'; + +interface IProtectedRoute { + children: React.ReactElement; +} + +export const RequireAuth = ({ children }: IProtectedRoute) => { + const jwt = useSelector((s: RootState) => s.user.userData?.jwt); + if (!jwt) return ; + + return children; +}; diff --git a/src/routes/routes.data.tsx b/src/routes/routes.data.tsx new file mode 100644 index 000000000..ea0814da5 --- /dev/null +++ b/src/routes/routes.data.tsx @@ -0,0 +1,63 @@ +import { createBrowserRouter } from 'react-router-dom'; +import Layout from 'src/shared/ui/layouts/Layout'; +import Main from 'src/widgets/main/Main'; +import UserProfile from 'src/pages/profile/UserProfile'; +import Error from 'src/pages/error/Error'; +import ListProductPage from 'src/pages/ListProduct/ListProductPage'; +import CartProduct from 'src/shared/ui/cartProduct/CartProduct'; +import { AddProductModal } from 'src/shared/ui/modals/modal/AddProductModal'; +import { EditProductModal } from 'src/shared/ui/modals/modal/EditProductModal'; +import { RequireAuth } from 'src/routes/helpers/RequireAuth'; +import React from 'react'; +import Login from 'src/pages/AuthScreen/SingInBlock/Login'; +import { ProtectedRouteAdmin } from './helpers/ProtectedRouteAdmin'; + +export const routes = createBrowserRouter([ + { + path: '/', + Component: Layout, + children: [ + { + path: '/', + Component: Main, + }, + { + path: '/userProfile', + element: ( + + + + ), + }, + { + path: '/trash', + Component: CartProduct, + }, + { + path: '/listProduct', + Component: ListProductPage, + }, + { + path: '/listProduct/edit', + element: ( + + + + ), + }, + { + path: '/listProduct/add', + element: ( + + + + ), + }, + ], + }, + { path: '/login', Component: Login }, + { + path: '*', + Component: Error, + }, +]); diff --git a/src/routes/routes.tsx b/src/routes/routes.tsx new file mode 100644 index 000000000..cabc74826 --- /dev/null +++ b/src/routes/routes.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import { BrowserRouter, Route, RouterProvider, Routes, useLocation } from 'react-router'; +import { routes } from 'src/routes/routes.data'; +import { EditProductModal } from 'src/shared/ui/modals/modal/EditProductModal'; +import Layout from 'src/shared/ui/layouts/Layout'; +import Main from 'src/widgets/main/Main'; +import { ListProduct } from 'src/shared/ui/listProduct'; +import ListProductPage from 'src/pages/ListProduct/ListProductPage'; +import { ProtectedRouteAdmin } from 'src/routes/helpers/ProtectedRouteAdmin'; +import { RequireAuth } from 'src/routes/helpers/RequireAuth'; +import UserProfile from 'src/pages/profile/UserProfile'; +import CartProduct from 'src/shared/ui/cartProduct/CartProduct'; +import Error from 'src/pages/error/Error'; +import { AddProductModal } from 'src/shared/ui/modals/modal/AddProductModal'; +import Login from 'src/pages/AuthScreen/SingInBlock/Login'; + +export const CustomRoutes = () => { + const location = useLocation(); + const previousLocation = location.state?.previousLocation; + + return ( + <> + + + + + + + } + /> + + + + + + + + {previousLocation && ( + + + + + } + /> + + + + } + /> + + )} + + ); +}; diff --git a/src/shared/.DS_Store b/src/shared/.DS_Store new file mode 100644 index 000000000..0bccecaca Binary files /dev/null and b/src/shared/.DS_Store differ diff --git a/src/shared/ui/.DS_Store b/src/shared/ui/.DS_Store new file mode 100644 index 000000000..78f8b0ed9 Binary files /dev/null and b/src/shared/ui/.DS_Store differ diff --git a/src/shared/ui/carousel/Carousel.stories.ts b/src/shared/ui/carousel/Carousel.stories.ts new file mode 100644 index 000000000..15fe8d36b --- /dev/null +++ b/src/shared/ui/carousel/Carousel.stories.ts @@ -0,0 +1,25 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { Carousel } from './Carousel'; +import { action } from '@storybook/addon-actions'; + +const meta: Meta = { + title: 'Homework2/Carousel', + component: Carousel, + tags: ['autodocs'], +}; + +export default meta; +type Story = StoryObj; + +export const DefaultCarousel: Story = { + args: { + // openModal: action('clicked'), + // handleNext: action('clicked'), + images: [ + '/free-icon-cleaning-9012135.png', + '/free-icon-cleaning-9717764.png', + '/yoga-mat-cleaning-kit.png', + '/cleaner.png', + ], + }, +}; diff --git a/src/shared/ui/carousel/Carousel.tsx b/src/shared/ui/carousel/Carousel.tsx new file mode 100644 index 000000000..957f742a1 --- /dev/null +++ b/src/shared/ui/carousel/Carousel.tsx @@ -0,0 +1,48 @@ +import React, { FC, useEffect, useLayoutEffect, useRef, useState } from 'react'; +import CarouselImage from './CarouselImage'; + +export const Carousel: FC<{ images: string[] }> = ({ images }) => { + const [index, setIndex] = useState(0); + + const handlePrev = () => { + if (index === 0) setIndex(images.length - 1); + else setIndex(index - 1); + }; + + const handleNext = () => { + if (index === images.length - 1) setIndex(0); + else setIndex(index + 1); + }; + + return ( +
+
+
+ {images.map((img: string, i: number) => ( + + ))} +
+ + +
+
+ ); +}; diff --git a/src/shared/ui/carousel/CarouselImage.tsx b/src/shared/ui/carousel/CarouselImage.tsx new file mode 100644 index 000000000..effc72870 --- /dev/null +++ b/src/shared/ui/carousel/CarouselImage.tsx @@ -0,0 +1,17 @@ +import React, { FC, memo } from 'react'; +import { clsx as cn } from 'clsx'; + +interface IImageCarousel { + image: string; + active: string; +} + +const CarouselImage = memo(function (image: IImageCarousel) { + return ( +
+ ... +
+ ); +}); +CarouselImage.displayName = 'CarouselImage'; +export default CarouselImage; diff --git a/src/shared/ui/cartButton/CartButton.stories.ts b/src/shared/ui/cartButton/CartButton.stories.ts new file mode 100644 index 000000000..dc73194e0 --- /dev/null +++ b/src/shared/ui/cartButton/CartButton.stories.ts @@ -0,0 +1,23 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { CartButton } from './CartButton'; + +const meta: Meta = { + title: 'Homework2/CartButton', + component: CartButton, + tags: ['autodocs'], +}; + +export default meta; +type Story = StoryObj; + +// export const CounterZero: Story = { +// args: { +// counter: 0, +// }, +// }; + +// export const CounterMoreZero: Story = { +// args: { +// counter: 1, +// }, +// }; diff --git a/src/shared/ui/cartButton/CartButton.tsx b/src/shared/ui/cartButton/CartButton.tsx new file mode 100644 index 000000000..70ca70eec --- /dev/null +++ b/src/shared/ui/cartButton/CartButton.tsx @@ -0,0 +1,43 @@ +import React, { useEffect, useState } from 'react'; +import { useDispatch } from 'react-redux'; +import ITrashButton from 'src/entities/interfaces/ICartButton'; +import { cartActions } from 'src/features/store/cart.slice'; +import { AppDispath } from 'src/features/store/store'; + +export const CartButton = ({ product }: ITrashButton) => { + const [counter, setCounter] = useState(0); + const dispatch = useDispatch(); + + const handleAddProduct = () => { + dispatch(cartActions.add(product)); + setCounter((prev) => prev + 1); + }; + + const handleIncrement = () => { + setCounter((prev) => prev + 1); + }; + + const handleDecrement = () => { + setCounter((prev) => prev - 1); + }; + + if (!counter) { + return ( + + ); + } + + return ( +
+ + - + + + + + + +
+ ); +}; diff --git a/src/shared/ui/cartButton/index.ts b/src/shared/ui/cartButton/index.ts new file mode 100644 index 000000000..5304ae5a1 --- /dev/null +++ b/src/shared/ui/cartButton/index.ts @@ -0,0 +1 @@ +export * from './CartButton'; diff --git a/src/shared/ui/cartProduct/CartProduct.stories.ts b/src/shared/ui/cartProduct/CartProduct.stories.ts new file mode 100644 index 000000000..a5b65c0d0 --- /dev/null +++ b/src/shared/ui/cartProduct/CartProduct.stories.ts @@ -0,0 +1,16 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import CartProduct from './CartProduct'; + +const meta: Meta = { + title: 'Homework2/CartProduct', + component: CartProduct, + tags: ['autodocs'], + parameters: { + layout: 'fullscreen', + }, +}; + +export default meta; +type Story = StoryObj; + +export const PrimaryCartProduct: Story = {}; diff --git a/src/shared/ui/cartProduct/CartProduct.tsx b/src/shared/ui/cartProduct/CartProduct.tsx new file mode 100644 index 000000000..15f7f0e7f --- /dev/null +++ b/src/shared/ui/cartProduct/CartProduct.tsx @@ -0,0 +1,28 @@ +import React, { FC } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { cartActions } from 'src/features/store/cart.slice'; +import { AppDispath, RootState } from 'src/features/store/store'; + +const CartProduct = () => { + const products = useSelector((s: RootState) => s.user.cartData.items); + const dispatch = useDispatch(); + + return ( +
+ {products.map((product) => ( +
+ ... +
+
{product.title}
+
{product.price}
+

{product.description}

+ +
+
+ ))} +
+ ); +}; +export default CartProduct; diff --git a/src/shared/ui/changeThemeButton/ChangeThemeButton.tsx b/src/shared/ui/changeThemeButton/ChangeThemeButton.tsx new file mode 100644 index 000000000..fe5419958 --- /dev/null +++ b/src/shared/ui/changeThemeButton/ChangeThemeButton.tsx @@ -0,0 +1,14 @@ +import { faMoon, faSun } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import React, { FC, useContext } from 'react'; +import { Link } from 'react-router'; +import { ThemeContext } from 'src/context/themeContext'; + +export const ChangeThemeButton: FC = () => { + const { theme, toggleTheme } = useContext(ThemeContext); + return ( + + {theme === 'light' ? : } + + ); +}; diff --git a/src/shared/ui/formProduct/FormProduct.stories.ts b/src/shared/ui/formProduct/FormProduct.stories.ts new file mode 100644 index 000000000..54beb6652 --- /dev/null +++ b/src/shared/ui/formProduct/FormProduct.stories.ts @@ -0,0 +1,13 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import FormProduct from './FormProduct'; + +const meta: Meta = { + title: 'Homework2/FormProduct', + component: FormProduct, + tags: ['autodocs'], +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/src/shared/ui/formProduct/FormProduct.tsx b/src/shared/ui/formProduct/FormProduct.tsx new file mode 100644 index 000000000..b0607a8db --- /dev/null +++ b/src/shared/ui/formProduct/FormProduct.tsx @@ -0,0 +1,81 @@ +import React, { FC } from 'react'; +import IFullProduct from 'src/entities/interfaces/IFullProduct'; +import { useForm, SubmitHandler } from 'react-hook-form'; +import cn from 'clsx'; +import { useDispatch } from 'react-redux'; +import { AppDispath } from 'src/features/store/store'; +import { productActions } from 'src/features/store/product.slice'; +import IShortProduct from 'src/entities/interfaces/IShortProduct'; +import { faker } from '@faker-js/faker'; + +const FormProduct = () => { + const { + register, + handleSubmit, + reset, + formState: { errors }, + } = useForm({ + mode: 'onChange', + }); + const dispatch = useDispatch(); + + const onSubmit: SubmitHandler = (data) => { + if (!data.id) data = { ...data, id: faker.number.int() }; + dispatch(productActions.add(data)); + reset(); + }; + + return ( +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ ); +}; +export default FormProduct; diff --git a/src/shared/ui/fullProduct/FullProduct.stories.ts b/src/shared/ui/fullProduct/FullProduct.stories.ts new file mode 100644 index 000000000..20936dbd1 --- /dev/null +++ b/src/shared/ui/fullProduct/FullProduct.stories.ts @@ -0,0 +1,21 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { FullProduct } from './FullProduct'; + +const meta: Meta = { + title: 'Homework2/FullProduct', + component: FullProduct, + tags: ['autodocs'], +}; + +export default meta; +type Story = StoryObj; + +export const PrimaryFullProduct: Story = { + args: { + images: ['/free-icon-cleaning-9012135.png', '/free-icon-cleaning-9717764.png'], + price: 10000, + title: 'Веник', + description: 'Веник с длиной ручкой', + category: 'товары для дома', + }, +}; diff --git a/src/shared/ui/fullProduct/FullProduct.tsx b/src/shared/ui/fullProduct/FullProduct.tsx new file mode 100644 index 000000000..99e4f7c00 --- /dev/null +++ b/src/shared/ui/fullProduct/FullProduct.tsx @@ -0,0 +1,25 @@ +import React, { FC, memo, useState } from 'react'; +import { CartButton } from '../cartButton'; +import IFullProduct from 'src/entities/interfaces/IFullProduct'; +import { useDispatch } from 'react-redux'; +import { AppDispath } from 'src/features/store/store'; +import { cartActions } from 'src/features/store/cart.slice'; + +export const FullProduct = memo(function FullProduct({ price, images, category, title, description }: IFullProduct) { + + return ( +
+ {images?.map((image, i) => ( + ... + ))} + +
+
{title}
+
{category}
+

{description}

+

цена: {price}р

+ {/* */} +
+
+ ); +}); diff --git a/src/shared/ui/fullProduct/index.ts b/src/shared/ui/fullProduct/index.ts new file mode 100644 index 000000000..1cfc170d6 --- /dev/null +++ b/src/shared/ui/fullProduct/index.ts @@ -0,0 +1 @@ +export * from './FullProduct'; diff --git a/src/shared/ui/header/Header.tsx b/src/shared/ui/header/Header.tsx new file mode 100644 index 000000000..2e0e0c2fc --- /dev/null +++ b/src/shared/ui/header/Header.tsx @@ -0,0 +1,83 @@ +import React, { FC, useContext, useEffect } from 'react'; +import { withTranslation } from 'react-i18next'; +import Logo from '../logo/Logo'; +import { LangSwitcher } from 'src/features/LangSwitcher/LangSwitcher'; + +import { Link, NavLink, useLocation, useNavigate } from 'react-router'; +import { ThemeContext } from 'src/context/themeContext'; +import { clsx as cn } from 'clsx'; +import { ChangeThemeButton } from '../changeThemeButton/ChangeThemeButton'; +import { useDispatch, useSelector } from 'react-redux'; +import { AppDispath, RootState } from 'src/features/store/store'; +import { userActions } from 'src/features/store/user.slice'; +import { productActions } from 'src/features/store/product.slice'; + +export const HeaderOrigin: FC = () => { + const dispatch = useDispatch(); + const count = useSelector((s: RootState) => s.user.cartData.items.length); + const navigate = useNavigate(); + const logout = () => { + dispatch(userActions.logout()); + navigate('/login'); + }; + const location = useLocation(); + return ( +
+ +
+ ); +}; +HeaderOrigin.displayName = 'HeaderOrigin'; +export const Header = withTranslation()(HeaderOrigin); diff --git a/src/shared/ui/header/index.ts b/src/shared/ui/header/index.ts new file mode 100644 index 000000000..266dec8a1 --- /dev/null +++ b/src/shared/ui/header/index.ts @@ -0,0 +1 @@ +export * from './Header'; diff --git a/src/shared/ui/layouts/Layout.css b/src/shared/ui/layouts/Layout.css new file mode 100644 index 000000000..2e7757075 --- /dev/null +++ b/src/shared/ui/layouts/Layout.css @@ -0,0 +1,9 @@ +.layout { + width: 1440px; + height: 1036px; + top: 162px; + left: 80px; + gap: 0px; + border-radius: 20px 20px 0px 0px; + opacity: 0px; +} \ No newline at end of file diff --git a/src/shared/ui/layouts/Layout.tsx b/src/shared/ui/layouts/Layout.tsx new file mode 100644 index 000000000..278763d37 --- /dev/null +++ b/src/shared/ui/layouts/Layout.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { Outlet } from 'react-router'; +import './Layout.css'; +import { Header } from '../header'; + +function Layout() { + return ( + <> +
+
+
+ +
+ + ); +} +export default Layout; diff --git a/src/shared/ui/listProduct/ListProduct.tsx b/src/shared/ui/listProduct/ListProduct.tsx new file mode 100644 index 000000000..4b70254a9 --- /dev/null +++ b/src/shared/ui/listProduct/ListProduct.tsx @@ -0,0 +1,15 @@ +import React, { FC, useEffect } from 'react'; + +import FuncProps from 'src/entities/interfaces/IFuncProps'; +import { FullProduct } from '../fullProduct'; +import ShortProduct from '../shortProduct/ShortProduct'; + +export const ListProduct: FC = ({ products }) => { + return ( + <> + {products.map((product, i: number) => ( + + ))} + + ); +}; diff --git a/src/shared/ui/listProduct/index.ts b/src/shared/ui/listProduct/index.ts new file mode 100644 index 000000000..95299735c --- /dev/null +++ b/src/shared/ui/listProduct/index.ts @@ -0,0 +1 @@ +export * from './ListProduct'; diff --git a/src/shared/ui/logo/Logo.tsx b/src/shared/ui/logo/Logo.tsx new file mode 100644 index 000000000..44e9f43f4 --- /dev/null +++ b/src/shared/ui/logo/Logo.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import logo from '../../../assets/logo-clean.png'; +function Logo() { + return ( +
+ +
+ ); +} +export default Logo; diff --git a/src/shared/ui/modalButton/ShowModalButton.tsx b/src/shared/ui/modalButton/ShowModalButton.tsx new file mode 100644 index 000000000..ade7e4856 --- /dev/null +++ b/src/shared/ui/modalButton/ShowModalButton.tsx @@ -0,0 +1,31 @@ +import React, { useState } from 'react'; +import Modal from '../modals/modal/Modal'; + +function ShowModalButton() { + const [visible, setVisible] = useState(false); + const [inputText, setInputText] = useState(''); + + const openModal = () => { + setVisible(true); + }; + const closeModal = () => { + setVisible(false); + }; + const modal = ( + +
aaaaa
+
+ ); + + return ( +
+ setInputText(e.target.value)} /> + + {visible && modal} +
+ ); +} + +export default ShowModalButton; diff --git a/src/shared/ui/modalButton/showModalButton.stories.ts b/src/shared/ui/modalButton/showModalButton.stories.ts new file mode 100644 index 000000000..e4bffcd22 --- /dev/null +++ b/src/shared/ui/modalButton/showModalButton.stories.ts @@ -0,0 +1,19 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { action } from '@storybook/addon-actions'; +import ShowModalButton from './ShowModalButton'; + +const meta: Meta = { + title: 'Homework2/ShowModalButton', + component: ShowModalButton, + tags: ['autodocs'], +}; + +export default meta; +type Story = StoryObj; + +export const ShowModal: Story = { + args: { + openModal: action('clicked'), + }, +}; diff --git a/src/shared/ui/modals/modal/AddProductModal.tsx b/src/shared/ui/modals/modal/AddProductModal.tsx new file mode 100644 index 000000000..c06f4c8ae --- /dev/null +++ b/src/shared/ui/modals/modal/AddProductModal.tsx @@ -0,0 +1,23 @@ +import React, { useEffect, useState } from 'react'; +import FormProduct from '../../formProduct/FormProduct'; +import Modal from './Modal'; +import { useNavigate } from 'react-router'; + +export const AddProductModal = () => { + const [visible, setVisible] = useState(false); + const navigate = useNavigate(); + useEffect(() => { + setVisible(true); + return () => setVisible(false); + }, []); + + const onClosed = () => { + navigate(-1); + }; + + return ( + + {} + + ); +}; diff --git a/src/shared/ui/modals/modal/EditProductModal.tsx b/src/shared/ui/modals/modal/EditProductModal.tsx new file mode 100644 index 000000000..3d9d63627 --- /dev/null +++ b/src/shared/ui/modals/modal/EditProductModal.tsx @@ -0,0 +1,24 @@ +import React, { useEffect, useState } from 'react'; +import FormProduct from '../../formProduct/FormProduct'; +import Modal from './Modal'; +import { useNavigate } from 'react-router'; + +export const EditProductModal = () => { + const [visible, setVisible] = useState(false); + const navigate = useNavigate(); + useEffect(() => { + setVisible(true); + return () => setVisible(false); + }, []); + + const onClosed = () => { + setVisible(false); + navigate(-1); + }; + + return ( + + {} + + ); +}; diff --git a/src/shared/ui/modals/modal/Modal.css b/src/shared/ui/modals/modal/Modal.css new file mode 100644 index 000000000..8c2731d40 --- /dev/null +++ b/src/shared/ui/modals/modal/Modal.css @@ -0,0 +1,7 @@ +.modal-container { + display: block !important; + width: 39%; + height: 88%; + left: 30%; + overflow: hidden; +} \ No newline at end of file diff --git a/src/shared/ui/modals/modal/Modal.tsx b/src/shared/ui/modals/modal/Modal.tsx new file mode 100644 index 000000000..93c1904a7 --- /dev/null +++ b/src/shared/ui/modals/modal/Modal.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import ModalOverlay from './ModalOverlay'; +import './Modal.css'; +import IModal from 'src/entities/interfaces/IModal'; + +const modalRoot = document.getElementById('root-modal'); + +function Modal({ visible, header = '', children, onClose }: IModal) { + return visible + ? ReactDOM.createPortal( + <> +
+
+
+
+
{header}
+ +
+
{children}
+
+
+
+ + , + modalRoot + ) + : null; +} + +export default Modal; diff --git a/src/shared/ui/modals/modal/ModalOverlay.css b/src/shared/ui/modals/modal/ModalOverlay.css new file mode 100644 index 000000000..9f5127f86 --- /dev/null +++ b/src/shared/ui/modals/modal/ModalOverlay.css @@ -0,0 +1,3 @@ +.modal-overlay { + opacity: 0.6 +} \ No newline at end of file diff --git a/src/shared/ui/modals/modal/ModalOverlay.tsx b/src/shared/ui/modals/modal/ModalOverlay.tsx new file mode 100644 index 000000000..220e9a7c2 --- /dev/null +++ b/src/shared/ui/modals/modal/ModalOverlay.tsx @@ -0,0 +1,8 @@ +import './ModalOverlay.css'; +import React from 'react'; + +function ModalOverlay() { + return
; +} + +export default ModalOverlay; diff --git a/src/shared/ui/shortProduct/ShortProduct.stories.ts b/src/shared/ui/shortProduct/ShortProduct.stories.ts new file mode 100644 index 000000000..2abe49e75 --- /dev/null +++ b/src/shared/ui/shortProduct/ShortProduct.stories.ts @@ -0,0 +1,23 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import ShortProduct from './ShortProduct'; + +const meta: Meta = { + title: 'Homework2/ShortProduct', + component: ShortProduct, + tags: ['autodocs'], + parameters: { + layout: 'fullscreen', + }, +}; + +export default meta; +type Story = StoryObj; + +export const PrimaryProduct: Story = { + args: { + title: 'Чистящие средства', + price: 5000, + description: 'Хорошо очищает любые загрязнения', + image: '/free-icon-household-7029117.png', + }, +}; diff --git a/src/shared/ui/shortProduct/ShortProduct.tsx b/src/shared/ui/shortProduct/ShortProduct.tsx new file mode 100644 index 000000000..824742832 --- /dev/null +++ b/src/shared/ui/shortProduct/ShortProduct.tsx @@ -0,0 +1,28 @@ +import React, { memo, useState } from 'react'; + +import IShortProduct from 'src/entities/interfaces/IShortProduct'; +import { CartButton } from '../cartButton'; +import { AppDispath } from 'src/features/store/store'; +import { useDispatch } from 'react-redux'; +import { cartActions } from 'src/features/store/cart.slice'; +import { ICartProduct } from 'src/entities/interfaces/ICartProduct'; + +const ShortProduct = memo(function ShortProduct({ id, title, price, description, image }: IShortProduct) { + const product = { id, title, price, description, image } as ICartProduct; + + return ( +
+ ... +
+
{title}
+
{price}
+

{description}

+
+
+ +
+
+ ); +}); + +export default ShortProduct; diff --git a/src/stories/Button.stories.ts b/src/stories/Examples/Button.stories.ts similarity index 100% rename from src/stories/Button.stories.ts rename to src/stories/Examples/Button.stories.ts diff --git a/src/stories/Button.tsx b/src/stories/Examples/Button.tsx similarity index 83% rename from src/stories/Button.tsx rename to src/stories/Examples/Button.tsx index 244ff033e..57c74e5b0 100644 --- a/src/stories/Button.tsx +++ b/src/stories/Examples/Button.tsx @@ -30,12 +30,7 @@ interface ButtonProps { export function Button({ primary = false, size = 'medium', backgroundColor, label, ...props }: ButtonProps) { const mode = primary ? s.primary : s.secondary; return ( - ); diff --git a/src/stories/Header.stories.ts b/src/stories/Examples/Header.stories.ts similarity index 100% rename from src/stories/Header.stories.ts rename to src/stories/Examples/Header.stories.ts diff --git a/src/stories/Header.tsx b/src/stories/Examples/Header.tsx similarity index 100% rename from src/stories/Header.tsx rename to src/stories/Examples/Header.tsx diff --git a/src/stories/Introduction.mdx b/src/stories/Examples/Introduction.mdx similarity index 100% rename from src/stories/Introduction.mdx rename to src/stories/Examples/Introduction.mdx diff --git a/src/stories/Page.stories.ts b/src/stories/Examples/Page.stories.ts similarity index 100% rename from src/stories/Page.stories.ts rename to src/stories/Examples/Page.stories.ts diff --git a/src/stories/Page.tsx b/src/stories/Examples/Page.tsx similarity index 100% rename from src/stories/Page.tsx rename to src/stories/Examples/Page.tsx diff --git a/src/stories/assets/code-brackets.svg b/src/stories/Examples/assets/code-brackets.svg similarity index 100% rename from src/stories/assets/code-brackets.svg rename to src/stories/Examples/assets/code-brackets.svg diff --git a/src/stories/assets/colors.svg b/src/stories/Examples/assets/colors.svg similarity index 100% rename from src/stories/assets/colors.svg rename to src/stories/Examples/assets/colors.svg diff --git a/src/stories/assets/comments.svg b/src/stories/Examples/assets/comments.svg similarity index 100% rename from src/stories/assets/comments.svg rename to src/stories/Examples/assets/comments.svg diff --git a/src/stories/assets/direction.svg b/src/stories/Examples/assets/direction.svg similarity index 100% rename from src/stories/assets/direction.svg rename to src/stories/Examples/assets/direction.svg diff --git a/src/stories/assets/flow.svg b/src/stories/Examples/assets/flow.svg similarity index 100% rename from src/stories/assets/flow.svg rename to src/stories/Examples/assets/flow.svg diff --git a/src/stories/assets/plugin.svg b/src/stories/Examples/assets/plugin.svg similarity index 100% rename from src/stories/assets/plugin.svg rename to src/stories/Examples/assets/plugin.svg diff --git a/src/stories/assets/repo.svg b/src/stories/Examples/assets/repo.svg similarity index 100% rename from src/stories/assets/repo.svg rename to src/stories/Examples/assets/repo.svg diff --git a/src/stories/assets/stackalt.svg b/src/stories/Examples/assets/stackalt.svg similarity index 100% rename from src/stories/assets/stackalt.svg rename to src/stories/Examples/assets/stackalt.svg diff --git a/src/stories/button.module.sass b/src/stories/Examples/button.module.sass similarity index 100% rename from src/stories/button.module.sass rename to src/stories/Examples/button.module.sass diff --git a/src/stories/header.css b/src/stories/Examples/header.css similarity index 100% rename from src/stories/header.css rename to src/stories/Examples/header.css diff --git a/src/stories/page.css b/src/stories/Examples/page.css similarity index 100% rename from src/stories/page.css rename to src/stories/Examples/page.css diff --git a/src/stories/Homework2/Collapse/Collapse.module.sass b/src/stories/Homework2/Collapse/Collapse.module.sass new file mode 100644 index 000000000..89ef38b09 --- /dev/null +++ b/src/stories/Homework2/Collapse/Collapse.module.sass @@ -0,0 +1,9 @@ + +.collapse + opacity:0 + transition: 0.3s + +.collapsing + opacity: 1 + overflow: hidden + transition: 0.3s \ No newline at end of file diff --git a/src/stories/Homework2/Collapse/Collapse.stories.ts b/src/stories/Homework2/Collapse/Collapse.stories.ts new file mode 100644 index 000000000..e55dfc19a --- /dev/null +++ b/src/stories/Homework2/Collapse/Collapse.stories.ts @@ -0,0 +1,18 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { Collapse } from './Collapse'; + +const meta: Meta = { + title: 'Homework2/Collapse', + component: Collapse, + tags: ['autodocs'], +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + opened: true, + children: `Hello Word`, + }, +}; diff --git a/src/stories/Homework2/Collapse/Collapse.tsx b/src/stories/Homework2/Collapse/Collapse.tsx new file mode 100644 index 000000000..ac765c71b --- /dev/null +++ b/src/stories/Homework2/Collapse/Collapse.tsx @@ -0,0 +1,19 @@ +import React, { FC } from 'react'; +import s from './Collapse.module.sass'; +import cn from 'clsx'; + +export type CollapseProps = { + className?: string; + children: React.ReactNode; + opened: boolean; +}; + +export const Collapse: FC = ({ className, opened, children }) => { + return ( + <> +
+
{children}
+
+ + ); +}; diff --git a/src/stories/Homework2/Logo/Logo.stories.ts b/src/stories/Homework2/Logo/Logo.stories.ts new file mode 100644 index 000000000..7b0caa7be --- /dev/null +++ b/src/stories/Homework2/Logo/Logo.stories.ts @@ -0,0 +1,12 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import Logo from './Logo'; + +const meta: Meta = { + title: 'Homework2/Logo', + component: Logo, +}; + +export default meta; +type Story = StoryObj; + +export const WithAnImage: Story = {}; diff --git a/src/stories/Homework2/Logo/Logo.tsx b/src/stories/Homework2/Logo/Logo.tsx new file mode 100644 index 000000000..7b8aa025f --- /dev/null +++ b/src/stories/Homework2/Logo/Logo.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import logo from '../assets/logo-image64.png'; +function Logo() { + return ( +
+ +
+ ); +} +export default Logo; diff --git a/src/stories/Homework2/assets/free-icon-cleaning-9012135.png b/src/stories/Homework2/assets/free-icon-cleaning-9012135.png new file mode 100644 index 000000000..de7edd177 Binary files /dev/null and b/src/stories/Homework2/assets/free-icon-cleaning-9012135.png differ diff --git a/src/stories/Homework2/assets/free-icon-cleaning-9717764.png b/src/stories/Homework2/assets/free-icon-cleaning-9717764.png new file mode 100644 index 000000000..18e0305e8 Binary files /dev/null and b/src/stories/Homework2/assets/free-icon-cleaning-9717764.png differ diff --git a/src/stories/Homework2/assets/free-icon-household-7029117.png b/src/stories/Homework2/assets/free-icon-household-7029117.png new file mode 100644 index 000000000..c27f94814 Binary files /dev/null and b/src/stories/Homework2/assets/free-icon-household-7029117.png differ diff --git a/src/stories/Homework2/assets/logo-image.png b/src/stories/Homework2/assets/logo-image.png new file mode 100644 index 000000000..6fc41c902 Binary files /dev/null and b/src/stories/Homework2/assets/logo-image.png differ diff --git a/src/stories/Homework2/assets/logo-image64.png b/src/stories/Homework2/assets/logo-image64.png new file mode 100644 index 000000000..0037316ad Binary files /dev/null and b/src/stories/Homework2/assets/logo-image64.png differ diff --git a/src/stories/Homework2/changeLang/ChangeLangComponent.tsx b/src/stories/Homework2/changeLang/ChangeLangComponent.tsx new file mode 100644 index 000000000..7fe7555bf --- /dev/null +++ b/src/stories/Homework2/changeLang/ChangeLangComponent.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { I18nextProvider, useTranslation } from 'react-i18next'; + +function ChangeLangComponent() { + const { t, i18n } = useTranslation(); + const lang = i18n.language === 'ru' ? 'en' : 'ru'; + + return ( + <> + +
+ {t(`teststorybook`)} + +
+
+ + ); +} +export default ChangeLangComponent; diff --git a/src/stories/Homework2/changeLang/changeLangComponent.stories.ts b/src/stories/Homework2/changeLang/changeLangComponent.stories.ts new file mode 100644 index 000000000..241026660 --- /dev/null +++ b/src/stories/Homework2/changeLang/changeLangComponent.stories.ts @@ -0,0 +1,18 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { action } from '@storybook/addon-actions'; +import ChangeLangComponent from './ChangeLangComponent'; + +const meta: Meta = { + title: 'Homework2/ChangeLangComponent', + component: ChangeLangComponent, + tags: ['autodocs'], +}; + +export default meta; +type Story = StoryObj; + +export const ChangeLangPrimary: Story = { + args: { + openModal: action('clicked'), + }, +}; diff --git a/src/stories/Homework2/changeThemeButton/ChangeThemeButton.tsx b/src/stories/Homework2/changeThemeButton/ChangeThemeButton.tsx new file mode 100644 index 000000000..39f9bbc97 --- /dev/null +++ b/src/stories/Homework2/changeThemeButton/ChangeThemeButton.tsx @@ -0,0 +1,15 @@ +import React, { FC, useState } from 'react'; + +const ChangeThemeButton: FC = () => { + const [theme, setTheme] = useState('primary'); + + const hadleTheme = () => { + setTheme((prevTheme) => (prevTheme === 'primary' ? 'danger' : 'primary')); + }; + return ( + + ); +}; +export default ChangeThemeButton; diff --git a/src/stories/Homework2/changeThemeButton/changeThemeButton.stories.ts b/src/stories/Homework2/changeThemeButton/changeThemeButton.stories.ts new file mode 100644 index 000000000..869067b2d --- /dev/null +++ b/src/stories/Homework2/changeThemeButton/changeThemeButton.stories.ts @@ -0,0 +1,18 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { action } from '@storybook/addon-actions'; +import ChangeThemeButton from './ChangeThemeButton'; + +const meta: Meta = { + title: 'Homework2/ChangeThemeButton', + component: ChangeThemeButton, + tags: ['autodocs'], +}; + +export default meta; +type Story = StoryObj; + +export const ChangeThemePrimary: Story = { + args: { + openModal: action('clicked'), + }, +}; diff --git a/src/stories/Homework2/headers/Header.stories.ts b/src/stories/Homework2/headers/Header.stories.ts new file mode 100644 index 000000000..19953f0b8 --- /dev/null +++ b/src/stories/Homework2/headers/Header.stories.ts @@ -0,0 +1,18 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { Header } from './Header'; + +const meta: Meta = { + title: 'Homework2/Header', + component: Header, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/react/writing-docs/autodocs + tags: ['autodocs'], + parameters: { + // More on how to position stories at: https://storybook.js.org/docs/react/configure/story-layout + layout: 'fullscreen', + }, +}; + +export default meta; +type Story = StoryObj; + +export const Primary: Story = {}; diff --git a/src/stories/Homework2/headers/Header.tsx b/src/stories/Homework2/headers/Header.tsx new file mode 100644 index 000000000..a536e85ec --- /dev/null +++ b/src/stories/Homework2/headers/Header.tsx @@ -0,0 +1,14 @@ +import React, { FC } from 'react'; +import Logo from '../Logo/Logo'; + +export const Header: FC = () => { + return ( +
+
+
+ +
+
+
+ ); +}; diff --git a/src/stories/Homework2/layouts/Layout.css b/src/stories/Homework2/layouts/Layout.css new file mode 100644 index 000000000..2e7757075 --- /dev/null +++ b/src/stories/Homework2/layouts/Layout.css @@ -0,0 +1,9 @@ +.layout { + width: 1440px; + height: 1036px; + top: 162px; + left: 80px; + gap: 0px; + border-radius: 20px 20px 0px 0px; + opacity: 0px; +} \ No newline at end of file diff --git a/src/stories/Homework2/layouts/Layout.stories.ts b/src/stories/Homework2/layouts/Layout.stories.ts new file mode 100644 index 000000000..cafdcab72 --- /dev/null +++ b/src/stories/Homework2/layouts/Layout.stories.ts @@ -0,0 +1,18 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import Layout from './Layout'; + +const meta: Meta = { + title: 'Homework2/Layout', + component: Layout, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/react/writing-docs/autodocs + tags: ['autodocs'], + parameters: { + // More on how to position stories at: https://storybook.js.org/docs/react/configure/story-layout + layout: 'fullscreen', + }, +}; + +export default meta; +type Story = StoryObj; + +export const Primary: Story = {}; diff --git a/src/stories/Homework2/layouts/Layout.tsx b/src/stories/Homework2/layouts/Layout.tsx new file mode 100644 index 000000000..1242bbf0d --- /dev/null +++ b/src/stories/Homework2/layouts/Layout.tsx @@ -0,0 +1,8 @@ +import React from 'react'; +import './Layout.css'; +import { Header } from '../headers/Header'; + +function Layout() { + return
; +} +export default Layout; diff --git a/src/stories/Homework2/modals/Modal.css b/src/stories/Homework2/modals/Modal.css new file mode 100644 index 000000000..0cfeac6ca --- /dev/null +++ b/src/stories/Homework2/modals/Modal.css @@ -0,0 +1,7 @@ +.modal-container { + display: block; + width: 39%; + height: 88%; + left: 30%; + overflow: hidden; +} \ No newline at end of file diff --git a/src/stories/Homework2/modals/Modal.stories.tsx b/src/stories/Homework2/modals/Modal.stories.tsx new file mode 100644 index 000000000..59dc1f60e --- /dev/null +++ b/src/stories/Homework2/modals/Modal.stories.tsx @@ -0,0 +1,22 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import Modal from './Modal'; +import React from 'react'; +import { action } from '@storybook/addon-actions'; +const meta: Meta = { + title: 'Homework2/Modal', + component: Modal, + tags: ['autodocs'], + parameters: { + layout: 'fullscreen', + }, +}; + +export default meta; +type Story = StoryObj; + +export const PrimaryModal: Story = { + args: { + children:
, + onClose: action('clicked'), + }, +}; diff --git a/src/stories/Homework2/modals/Modal.tsx b/src/stories/Homework2/modals/Modal.tsx new file mode 100644 index 000000000..aaef608a4 --- /dev/null +++ b/src/stories/Homework2/modals/Modal.tsx @@ -0,0 +1,36 @@ +import React, { useState } from 'react'; +import ReactDOM from 'react-dom'; +import ModalOverlay from './ModalOverlay'; +import './Modal.css'; +import IModal from '../../../entities/interfaces/IModal'; + +// const modalRoot = document.getElementById('root-modal'); + +function Modal({ visible, header = '', children, onClose }: IModal) { + // return ReactDOM.createPortal( + return ( + <> +
+
+
+
+
+

{header}

+
+

+ × +

+
+
{children}
+
+
+
+ + + ); + // , + // modalRoot + // ); +} + +export default Modal; diff --git a/src/stories/Homework2/modals/ModalOverlay.css b/src/stories/Homework2/modals/ModalOverlay.css new file mode 100644 index 000000000..9f5127f86 --- /dev/null +++ b/src/stories/Homework2/modals/ModalOverlay.css @@ -0,0 +1,3 @@ +.modal-overlay { + opacity: 0.6 +} \ No newline at end of file diff --git a/src/stories/Homework2/modals/ModalOverlay.stories.ts b/src/stories/Homework2/modals/ModalOverlay.stories.ts new file mode 100644 index 000000000..2ec699771 --- /dev/null +++ b/src/stories/Homework2/modals/ModalOverlay.stories.ts @@ -0,0 +1,16 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import ModalOverlay from './ModalOverlay'; + +const meta: Meta = { + title: 'Homework2/ModalOverlay', + component: ModalOverlay, + tags: ['autodocs'], + parameters: { + layout: 'fullscreen', + }, +}; + +export default meta; +type Story = StoryObj; + +export const PrimaryModalOverlay: Story = {}; diff --git a/src/stories/Homework2/modals/ModalOverlay.tsx b/src/stories/Homework2/modals/ModalOverlay.tsx new file mode 100644 index 000000000..220e9a7c2 --- /dev/null +++ b/src/stories/Homework2/modals/ModalOverlay.tsx @@ -0,0 +1,8 @@ +import './ModalOverlay.css'; +import React from 'react'; + +function ModalOverlay() { + return
; +} + +export default ModalOverlay; diff --git a/src/widgets/main/Main.tsx b/src/widgets/main/Main.tsx new file mode 100644 index 000000000..dacef9a67 --- /dev/null +++ b/src/widgets/main/Main.tsx @@ -0,0 +1,79 @@ +import React, { useEffect, useState } from 'react'; +import logo from '../../app/logo.svg'; +import { useTranslation } from 'react-i18next'; +import { createRandomProduct } from 'src/homeworks/ts1/3_write'; +import { useInView } from 'react-intersection-observer'; +import { ListProduct } from 'src/shared/ui/listProduct'; +import Modal from 'src/shared/ui/modals/modal/Modal'; + +export default function Main() { + const [visible, setVisible] = useState(false); + const [products, setProducts] = useState([]); + const { t } = useTranslation(); + const { ref, inView } = useInView({ threshold: 0.7 }); + + function addProduct() { + const createdAt = '2023-06-06T12:06:56.957Z'; + const product = createRandomProduct(createdAt); + + setProducts([ + ...products, + { + price: product.price, + category: product.category.name, + title: product.name, + description: product.desc, + images: [product.photo], + }, + ]); + } + + // useEffect(() => { + // addProduct(); + // }, []); + + useEffect(() => { + if (inView) { + addProduct(); + } + }, [addProduct, inView]); + + const onOpen = () => { + setVisible(true); + }; + const onClosed = () => { + setProducts([]); + setVisible(false); + }; + + const modal = ( + +
+ +
+
+ +
+
+ ); + + return ( +
+
+ logo +
    +
  • {t(`key1`)}
  • +
  • {t(`key2`)}
  • +
  • {t(`key3`)}
  • +
  • {t(`key4`)}
  • +
+ + {modal} +
+
+ ); +} diff --git a/tsconfig.json b/tsconfig.json index 1fd6f7887..e3c7925d7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,23 +1,27 @@ { - "compilerOptions": { - "outDir": "./dist", - "baseUrl": ".", - "paths": { - "src/*": ["src/*"] - }, - "lib": ["dom", "dom.iterable", "esnext"], - "allowJs": true, - "allowSyntheticDefaultImports": true, - "skipLibCheck": true, - "esModuleInterop": true, - "target": "ES2015", - "module": "esnext", - "sourceMap": true, - "noImplicitAny": true, - "downlevelIteration": true, - "moduleResolution": "node", - "resolveJsonModule": true, - "jsx": "react" - } + "compilerOptions": { + "outDir": "./dist", + "baseUrl": ".", + "paths": { + "src/*": ["src/*"], + "*": ["types/*"], + "@components/*": ["src/components/*"], + "@images/*": ["scr/assets/*"], + "@interfaces/*": ["scr/entities/interfaces/*"] + }, + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true, + "esModuleInterop": true, + "target": "ES2015", + "module": "esnext", + "sourceMap": true, + "noImplicitAny": true, + "downlevelIteration": true, + "declaration": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "jsx": "react" + } } - \ No newline at end of file diff --git a/webpack.config.js b/webpack.config.js index 583dfd1ed..da14a8407 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -3,10 +3,12 @@ const HtmlWebpackPlugin = require('html-webpack-plugin'); const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); const { CleanWebpackPlugin } = require('clean-webpack-plugin'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); +const CopyWebPackPlugin = require('copy-webpack-plugin'); const port = 2233; const dist = path.join(__dirname, 'dist'); const src = path.join(__dirname, 'src'); +const assets = path.join(src, 'assets'); const host = 'localhost'; module.exports = (_, args) => { @@ -15,6 +17,11 @@ module.exports = (_, args) => { context: src, devServer: { open: true, + open: { + app: { + name: 'google chrome', + }, + }, port, hot: true, historyApiFallback: true, @@ -62,6 +69,10 @@ module.exports = (_, args) => { 'css-loader', ], }, + { + test: /\.(png|svg|jpg|jpeg|gif)$/i, + type: 'asset/resource', + }, { test: /\.svg/, type: 'asset/inline', @@ -100,6 +111,9 @@ module.exports = (_, args) => { configFile: path.join(__dirname, 'tsconfig.json'), }, }), + new CopyWebPackPlugin({ + patterns: [{ from: assets, to: dist, force: true }], + }), ], }; };