Browse Source

category page

viktoriia.kapran 2 năm trước cách đây
mục cha
commit
a9311df6d3
34 tập tin đã thay đổi với 1492 bổ sung496 xóa
  1. 828 0
      js21 react/my-react-app/package-lock.json
  2. 5 0
      js21 react/my-react-app/package.json
  3. 5 4
      js21 react/my-react-app/src/App.js
  4. 172 166
      js21 react/my-react-app/src/App.scss
  5. 150 0
      js21 react/my-react-app/src/api/gql.js
  6. 4 4
      js21 react/my-react-app/src/components/Button/Button.js
  7. 0 14
      js21 react/my-react-app/src/components/Button/Button.scss
  8. 19 8
      js21 react/my-react-app/src/components/Categories/CategoryMenu.js
  9. 5 0
      js21 react/my-react-app/src/components/Categories/CategoryMenu.scss
  10. 2 2
      js21 react/my-react-app/src/components/CategoriesSection/CategoriesSection.js
  11. 3 5
      js21 react/my-react-app/src/components/CategoriesSection/CategoriesSection.scss
  12. 0 8
      js21 react/my-react-app/src/components/Caunter/Caunter.js
  13. 17 0
      js21 react/my-react-app/src/components/Counter/Counter.js
  14. 7 0
      js21 react/my-react-app/src/components/Counter/Counter.scss
  15. 25 0
      js21 react/my-react-app/src/components/GoodCart/GoodCart.js
  16. 17 0
      js21 react/my-react-app/src/components/GoodCart/GoodCart.scss
  17. 22 7
      js21 react/my-react-app/src/components/Header/Header.js
  18. 32 34
      js21 react/my-react-app/src/components/Header/Header.scss
  19. 1 5
      js21 react/my-react-app/src/components/Image/Image.js
  20. 5 2
      js21 react/my-react-app/src/components/Layout/Layout.js
  21. 1 0
      js21 react/my-react-app/src/components/Layout/Layout.scss
  22. 11 0
      js21 react/my-react-app/src/components/Loader.js
  23. 1 1
      js21 react/my-react-app/src/components/Price/Price.js
  24. 1 1
      js21 react/my-react-app/src/components/Price/Price.scss
  25. 2 2
      js21 react/my-react-app/src/components/Title/Title.js
  26. 0 1
      js21 react/my-react-app/src/components/Title/Title.scss
  27. 0 16
      js21 react/my-react-app/src/functions/styles.scss
  28. 6 5
      js21 react/my-react-app/src/index.scss
  29. 0 18
      js21 react/my-react-app/src/pages/Category.js
  30. 35 0
      js21 react/my-react-app/src/pages/Category/Category.js
  31. 7 0
      js21 react/my-react-app/src/pages/Category/Category.scss
  32. 36 183
      js21 react/my-react-app/src/redux/actions/actions.js
  33. 18 10
      js21 react/my-react-app/src/redux/reducers/index.js
  34. 55 0
      js21 react/my-react-app/src/redux/slices/categoriesSlice.js

+ 828 - 0
js21 react/my-react-app/package-lock.json

@@ -8,6 +8,11 @@
       "name": "my-react-app",
       "version": "0.1.0",
       "dependencies": {
+        "@emotion/react": "^11.10.6",
+        "@emotion/styled": "^11.10.6",
+        "@mui/icons-material": "^5.11.11",
+        "@mui/material": "^5.11.11",
+        "@reduxjs/toolkit": "^1.9.3",
         "@testing-library/jest-dom": "^5.16.5",
         "@testing-library/react": "^13.4.0",
         "@testing-library/user-event": "^13.5.0",
@@ -2135,6 +2140,158 @@
         "postcss-selector-parser": "^6.0.10"
       }
     },
+    "node_modules/@emotion/babel-plugin": {
+      "version": "11.10.6",
+      "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.10.6.tgz",
+      "integrity": "sha512-p2dAqtVrkhSa7xz1u/m9eHYdLi+en8NowrmXeF/dKtJpU8lCWli8RUAati7NcSl0afsBott48pdnANuD0wh9QQ==",
+      "dependencies": {
+        "@babel/helper-module-imports": "^7.16.7",
+        "@babel/runtime": "^7.18.3",
+        "@emotion/hash": "^0.9.0",
+        "@emotion/memoize": "^0.8.0",
+        "@emotion/serialize": "^1.1.1",
+        "babel-plugin-macros": "^3.1.0",
+        "convert-source-map": "^1.5.0",
+        "escape-string-regexp": "^4.0.0",
+        "find-root": "^1.1.0",
+        "source-map": "^0.5.7",
+        "stylis": "4.1.3"
+      }
+    },
+    "node_modules/@emotion/babel-plugin/node_modules/escape-string-regexp": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+      "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/@emotion/babel-plugin/node_modules/source-map": {
+      "version": "0.5.7",
+      "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+      "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/@emotion/cache": {
+      "version": "11.10.5",
+      "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.10.5.tgz",
+      "integrity": "sha512-dGYHWyzTdmK+f2+EnIGBpkz1lKc4Zbj2KHd4cX3Wi8/OWr5pKslNjc3yABKH4adRGCvSX4VDC0i04mrrq0aiRA==",
+      "dependencies": {
+        "@emotion/memoize": "^0.8.0",
+        "@emotion/sheet": "^1.2.1",
+        "@emotion/utils": "^1.2.0",
+        "@emotion/weak-memoize": "^0.3.0",
+        "stylis": "4.1.3"
+      }
+    },
+    "node_modules/@emotion/hash": {
+      "version": "0.9.0",
+      "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.0.tgz",
+      "integrity": "sha512-14FtKiHhy2QoPIzdTcvh//8OyBlknNs2nXRwIhG904opCby3l+9Xaf/wuPvICBF0rc1ZCNBd3nKe9cd2mecVkQ=="
+    },
+    "node_modules/@emotion/is-prop-valid": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.0.tgz",
+      "integrity": "sha512-3aDpDprjM0AwaxGE09bOPkNxHpBd+kA6jty3RnaEXdweX1DF1U3VQpPYb0g1IStAuK7SVQ1cy+bNBBKp4W3Fjg==",
+      "dependencies": {
+        "@emotion/memoize": "^0.8.0"
+      }
+    },
+    "node_modules/@emotion/memoize": {
+      "version": "0.8.0",
+      "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.0.tgz",
+      "integrity": "sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA=="
+    },
+    "node_modules/@emotion/react": {
+      "version": "11.10.6",
+      "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.10.6.tgz",
+      "integrity": "sha512-6HT8jBmcSkfzO7mc+N1L9uwvOnlcGoix8Zn7srt+9ga0MjREo6lRpuVX0kzo6Jp6oTqDhREOFsygN6Ew4fEQbw==",
+      "dependencies": {
+        "@babel/runtime": "^7.18.3",
+        "@emotion/babel-plugin": "^11.10.6",
+        "@emotion/cache": "^11.10.5",
+        "@emotion/serialize": "^1.1.1",
+        "@emotion/use-insertion-effect-with-fallbacks": "^1.0.0",
+        "@emotion/utils": "^1.2.0",
+        "@emotion/weak-memoize": "^0.3.0",
+        "hoist-non-react-statics": "^3.3.1"
+      },
+      "peerDependencies": {
+        "react": ">=16.8.0"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@emotion/serialize": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.1.tgz",
+      "integrity": "sha512-Zl/0LFggN7+L1liljxXdsVSVlg6E/Z/olVWpfxUTxOAmi8NU7YoeWeLfi1RmnB2TATHoaWwIBRoL+FvAJiTUQA==",
+      "dependencies": {
+        "@emotion/hash": "^0.9.0",
+        "@emotion/memoize": "^0.8.0",
+        "@emotion/unitless": "^0.8.0",
+        "@emotion/utils": "^1.2.0",
+        "csstype": "^3.0.2"
+      }
+    },
+    "node_modules/@emotion/sheet": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.1.tgz",
+      "integrity": "sha512-zxRBwl93sHMsOj4zs+OslQKg/uhF38MB+OMKoCrVuS0nyTkqnau+BM3WGEoOptg9Oz45T/aIGs1qbVAsEFo3nA=="
+    },
+    "node_modules/@emotion/styled": {
+      "version": "11.10.6",
+      "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.10.6.tgz",
+      "integrity": "sha512-OXtBzOmDSJo5Q0AFemHCfl+bUueT8BIcPSxu0EGTpGk6DmI5dnhSzQANm1e1ze0YZL7TDyAyy6s/b/zmGOS3Og==",
+      "dependencies": {
+        "@babel/runtime": "^7.18.3",
+        "@emotion/babel-plugin": "^11.10.6",
+        "@emotion/is-prop-valid": "^1.2.0",
+        "@emotion/serialize": "^1.1.1",
+        "@emotion/use-insertion-effect-with-fallbacks": "^1.0.0",
+        "@emotion/utils": "^1.2.0"
+      },
+      "peerDependencies": {
+        "@emotion/react": "^11.0.0-rc.0",
+        "react": ">=16.8.0"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@emotion/unitless": {
+      "version": "0.8.0",
+      "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.0.tgz",
+      "integrity": "sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw=="
+    },
+    "node_modules/@emotion/use-insertion-effect-with-fallbacks": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.0.tgz",
+      "integrity": "sha512-1eEgUGmkaljiBnRMTdksDV1W4kUnmwgp7X9G8B++9GYwl1lUdqSndSriIrTJ0N7LQaoauY9JJ2yhiOYK5+NI4A==",
+      "peerDependencies": {
+        "react": ">=16.8.0"
+      }
+    },
+    "node_modules/@emotion/utils": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.0.tgz",
+      "integrity": "sha512-sn3WH53Kzpw8oQ5mgMmIzzyAaH2ZqFEbozVVBSYp538E06OSE6ytOp7pRAjNQR+Q/orwqdQYJSe2m3hCOeznkw=="
+    },
+    "node_modules/@emotion/weak-memoize": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.0.tgz",
+      "integrity": "sha512-AHPmaAx+RYfZz0eYu6Gviiagpmiyw98ySSlQvCUhVGDRtDFe4DBS0x1bSjdF3gqUDYOczB+yYvBTtEylYSdRhg=="
+    },
     "node_modules/@eslint/eslintrc": {
       "version": "1.4.1",
       "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.4.1.tgz",
@@ -2979,6 +3136,262 @@
       "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz",
       "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A=="
     },
+    "node_modules/@mui/base": {
+      "version": "5.0.0-alpha.119",
+      "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-alpha.119.tgz",
+      "integrity": "sha512-XA5zhlYfXi67u613eIF0xRmktkatx6ERy3h+PwrMN5IcWFbgiL1guz8VpdXON+GWb8+G7B8t5oqTFIaCqaSAeA==",
+      "dependencies": {
+        "@babel/runtime": "^7.21.0",
+        "@emotion/is-prop-valid": "^1.2.0",
+        "@mui/types": "^7.2.3",
+        "@mui/utils": "^5.11.11",
+        "@popperjs/core": "^2.11.6",
+        "clsx": "^1.2.1",
+        "prop-types": "^15.8.1",
+        "react-is": "^18.2.0"
+      },
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/mui"
+      },
+      "peerDependencies": {
+        "@types/react": "^17.0.0 || ^18.0.0",
+        "react": "^17.0.0 || ^18.0.0",
+        "react-dom": "^17.0.0 || ^18.0.0"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@mui/base/node_modules/react-is": {
+      "version": "18.2.0",
+      "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
+      "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w=="
+    },
+    "node_modules/@mui/core-downloads-tracker": {
+      "version": "5.11.11",
+      "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.11.11.tgz",
+      "integrity": "sha512-0YK0K9GfW1ysw9z4ztWAjLW+bktf+nExMyn2+EQe1Ijb0kF2kz1kIOmb4+di0/PsXG70uCuw4DhEIdNd+JQkRA==",
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/mui"
+      }
+    },
+    "node_modules/@mui/icons-material": {
+      "version": "5.11.11",
+      "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.11.11.tgz",
+      "integrity": "sha512-Eell3ADmQVE8HOpt/LZ3zIma8JSvPh3XgnhwZLT0k5HRqZcd6F/QDHc7xsWtgz09t+UEFvOYJXjtrwKmLdwwpw==",
+      "dependencies": {
+        "@babel/runtime": "^7.21.0"
+      },
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/mui"
+      },
+      "peerDependencies": {
+        "@mui/material": "^5.0.0",
+        "@types/react": "^17.0.0 || ^18.0.0",
+        "react": "^17.0.0 || ^18.0.0"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@mui/material": {
+      "version": "5.11.11",
+      "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.11.11.tgz",
+      "integrity": "sha512-sSe0dmKjB1IGOYt32Pcha+cXV3IIrX5L5mFAF9LDRssp/x53bluhgLLbkc8eTiJvueVvo6HAyze6EkFEYLQRXQ==",
+      "dependencies": {
+        "@babel/runtime": "^7.21.0",
+        "@mui/base": "5.0.0-alpha.119",
+        "@mui/core-downloads-tracker": "^5.11.11",
+        "@mui/system": "^5.11.11",
+        "@mui/types": "^7.2.3",
+        "@mui/utils": "^5.11.11",
+        "@types/react-transition-group": "^4.4.5",
+        "clsx": "^1.2.1",
+        "csstype": "^3.1.1",
+        "prop-types": "^15.8.1",
+        "react-is": "^18.2.0",
+        "react-transition-group": "^4.4.5"
+      },
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/mui"
+      },
+      "peerDependencies": {
+        "@emotion/react": "^11.5.0",
+        "@emotion/styled": "^11.3.0",
+        "@types/react": "^17.0.0 || ^18.0.0",
+        "react": "^17.0.0 || ^18.0.0",
+        "react-dom": "^17.0.0 || ^18.0.0"
+      },
+      "peerDependenciesMeta": {
+        "@emotion/react": {
+          "optional": true
+        },
+        "@emotion/styled": {
+          "optional": true
+        },
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@mui/material/node_modules/react-is": {
+      "version": "18.2.0",
+      "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
+      "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w=="
+    },
+    "node_modules/@mui/private-theming": {
+      "version": "5.11.11",
+      "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.11.11.tgz",
+      "integrity": "sha512-yLgTkjNC1mpye2SOUkc+zQQczUpg8NvQAETvxwXTMzNgJK1pv4htL7IvBM5vmCKG7IHAB3hX26W2u6i7bxwF3A==",
+      "dependencies": {
+        "@babel/runtime": "^7.21.0",
+        "@mui/utils": "^5.11.11",
+        "prop-types": "^15.8.1"
+      },
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/mui"
+      },
+      "peerDependencies": {
+        "@types/react": "^17.0.0 || ^18.0.0",
+        "react": "^17.0.0 || ^18.0.0"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@mui/styled-engine": {
+      "version": "5.11.11",
+      "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.11.11.tgz",
+      "integrity": "sha512-wV0UgW4lN5FkDBXefN8eTYeuE9sjyQdg5h94vtwZCUamGQEzmCOtir4AakgmbWMy0x8OLjdEUESn9wnf5J9MOg==",
+      "dependencies": {
+        "@babel/runtime": "^7.21.0",
+        "@emotion/cache": "^11.10.5",
+        "csstype": "^3.1.1",
+        "prop-types": "^15.8.1"
+      },
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/mui"
+      },
+      "peerDependencies": {
+        "@emotion/react": "^11.4.1",
+        "@emotion/styled": "^11.3.0",
+        "react": "^17.0.0 || ^18.0.0"
+      },
+      "peerDependenciesMeta": {
+        "@emotion/react": {
+          "optional": true
+        },
+        "@emotion/styled": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@mui/system": {
+      "version": "5.11.11",
+      "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.11.11.tgz",
+      "integrity": "sha512-a9gaOAJBjpzypDfhbGZQ8HzdcxdxsKkFvbp1aAWZhFHBPdehEkARNh7mj851VfEhD/GdffYt85PFKFKdUta5Eg==",
+      "dependencies": {
+        "@babel/runtime": "^7.21.0",
+        "@mui/private-theming": "^5.11.11",
+        "@mui/styled-engine": "^5.11.11",
+        "@mui/types": "^7.2.3",
+        "@mui/utils": "^5.11.11",
+        "clsx": "^1.2.1",
+        "csstype": "^3.1.1",
+        "prop-types": "^15.8.1"
+      },
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/mui"
+      },
+      "peerDependencies": {
+        "@emotion/react": "^11.5.0",
+        "@emotion/styled": "^11.3.0",
+        "@types/react": "^17.0.0 || ^18.0.0",
+        "react": "^17.0.0 || ^18.0.0"
+      },
+      "peerDependenciesMeta": {
+        "@emotion/react": {
+          "optional": true
+        },
+        "@emotion/styled": {
+          "optional": true
+        },
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@mui/types": {
+      "version": "7.2.3",
+      "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.3.tgz",
+      "integrity": "sha512-tZ+CQggbe9Ol7e/Fs5RcKwg/woU+o8DCtOnccX6KmbBc7YrfqMYEYuaIcXHuhpT880QwNkZZ3wQwvtlDFA2yOw==",
+      "peerDependencies": {
+        "@types/react": "*"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@mui/utils": {
+      "version": "5.11.11",
+      "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.11.11.tgz",
+      "integrity": "sha512-neMM5rrEXYQrOrlxUfns/TGgX4viS8K2zb9pbQh11/oUUYFlGI32Tn+PHePQx7n6Fy/0zq6WxdBFC9VpnJ5JrQ==",
+      "dependencies": {
+        "@babel/runtime": "^7.21.0",
+        "@types/prop-types": "^15.7.5",
+        "@types/react-is": "^16.7.1 || ^17.0.0",
+        "prop-types": "^15.8.1",
+        "react-is": "^18.2.0"
+      },
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/mui"
+      },
+      "peerDependencies": {
+        "react": "^17.0.0 || ^18.0.0"
+      }
+    },
+    "node_modules/@mui/utils/node_modules/react-is": {
+      "version": "18.2.0",
+      "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
+      "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w=="
+    },
     "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": {
       "version": "5.1.1-v1",
       "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz",
@@ -3088,6 +3501,38 @@
         }
       }
     },
+    "node_modules/@popperjs/core": {
+      "version": "2.11.6",
+      "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz",
+      "integrity": "sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==",
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/popperjs"
+      }
+    },
+    "node_modules/@reduxjs/toolkit": {
+      "version": "1.9.3",
+      "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.3.tgz",
+      "integrity": "sha512-GU2TNBQVofL09VGmuSioNPQIu6Ml0YLf4EJhgj0AvBadRlCGzUWet8372LjvO4fqKZF2vH1xU0htAa7BrK9pZg==",
+      "dependencies": {
+        "immer": "^9.0.16",
+        "redux": "^4.2.0",
+        "redux-thunk": "^2.4.2",
+        "reselect": "^4.1.7"
+      },
+      "peerDependencies": {
+        "react": "^16.9.0 || ^17.0.0 || ^18",
+        "react-redux": "^7.2.1 || ^8.0.2"
+      },
+      "peerDependenciesMeta": {
+        "react": {
+          "optional": true
+        },
+        "react-redux": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/@remix-run/router": {
       "version": "1.3.3",
       "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.3.3.tgz",
@@ -4190,6 +4635,22 @@
         "@types/react": "*"
       }
     },
+    "node_modules/@types/react-is": {
+      "version": "17.0.3",
+      "resolved": "https://registry.npmjs.org/@types/react-is/-/react-is-17.0.3.tgz",
+      "integrity": "sha512-aBTIWg1emtu95bLTLx0cpkxwGW3ueZv71nE2YFBpL8k/z5czEW8yYpOo8Dp+UUAFAtKwNaOsh/ioSeQnWlZcfw==",
+      "dependencies": {
+        "@types/react": "*"
+      }
+    },
+    "node_modules/@types/react-transition-group": {
+      "version": "4.4.5",
+      "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.5.tgz",
+      "integrity": "sha512-juKD/eiSM3/xZYzjuzH6ZwpP+/lejltmiS3QEzV/vmb/Q8+HfDmxu+Baga8UEMGBqV88Nbg4l2hY/K2DkyaLLA==",
+      "dependencies": {
+        "@types/react": "*"
+      }
+    },
     "node_modules/@types/resolve": {
       "version": "1.17.1",
       "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz",
@@ -5764,6 +6225,14 @@
         "wrap-ansi": "^7.0.0"
       }
     },
+    "node_modules/clsx": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
+      "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
+      "engines": {
+        "node": ">=6"
+      }
+    },
     "node_modules/co": {
       "version": "4.6.0",
       "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
@@ -6692,6 +7161,15 @@
         "utila": "~0.4"
       }
     },
+    "node_modules/dom-helpers": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
+      "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
+      "dependencies": {
+        "@babel/runtime": "^7.8.7",
+        "csstype": "^3.0.2"
+      }
+    },
     "node_modules/dom-serializer": {
       "version": "1.4.1",
       "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz",
@@ -8089,6 +8567,11 @@
         "url": "https://github.com/avajs/find-cache-dir?sponsor=1"
       }
     },
+    "node_modules/find-root": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
+      "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng=="
+    },
     "node_modules/find-up": {
       "version": "5.0.0",
       "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
@@ -14512,6 +14995,21 @@
         }
       }
     },
+    "node_modules/react-transition-group": {
+      "version": "4.4.5",
+      "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
+      "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
+      "dependencies": {
+        "@babel/runtime": "^7.5.5",
+        "dom-helpers": "^5.0.1",
+        "loose-envify": "^1.4.0",
+        "prop-types": "^15.6.2"
+      },
+      "peerDependencies": {
+        "react": ">=16.6.0",
+        "react-dom": ">=16.6.0"
+      }
+    },
     "node_modules/read-cache": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@@ -14720,6 +15218,11 @@
       "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
       "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="
     },
+    "node_modules/reselect": {
+      "version": "4.1.7",
+      "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.7.tgz",
+      "integrity": "sha512-Zu1xbUt3/OPwsXL46hvOOoQrap2azE7ZQbokq61BQfiXvhewsKDwhMeZjTX9sX0nvw1t/U5Audyn1I9P/m9z0A=="
+    },
     "node_modules/resolve": {
       "version": "1.22.1",
       "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz",
@@ -15662,6 +16165,11 @@
         "postcss": "^8.2.15"
       }
     },
+    "node_modules/stylis": {
+      "version": "4.1.3",
+      "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.1.3.tgz",
+      "integrity": "sha512-GP6WDNWf+o403jrEp9c5jibKavrtLW+/qYGhFxFrG8maXhwTBI7gLLhiBb0o7uFccWN+EOS9aMO6cGHWAO07OA=="
+    },
     "node_modules/supports-color": {
       "version": "5.5.0",
       "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
@@ -18717,6 +19225,132 @@
       "integrity": "sha512-jwx+WCqszn53YHOfvFMJJRd/B2GqkCBt+1MJSG6o5/s8+ytHMvDZXsJgUEWLk12UnLd7HYKac4BYU5i/Ron1Cw==",
       "requires": {}
     },
+    "@emotion/babel-plugin": {
+      "version": "11.10.6",
+      "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.10.6.tgz",
+      "integrity": "sha512-p2dAqtVrkhSa7xz1u/m9eHYdLi+en8NowrmXeF/dKtJpU8lCWli8RUAati7NcSl0afsBott48pdnANuD0wh9QQ==",
+      "requires": {
+        "@babel/helper-module-imports": "^7.16.7",
+        "@babel/runtime": "^7.18.3",
+        "@emotion/hash": "^0.9.0",
+        "@emotion/memoize": "^0.8.0",
+        "@emotion/serialize": "^1.1.1",
+        "babel-plugin-macros": "^3.1.0",
+        "convert-source-map": "^1.5.0",
+        "escape-string-regexp": "^4.0.0",
+        "find-root": "^1.1.0",
+        "source-map": "^0.5.7",
+        "stylis": "4.1.3"
+      },
+      "dependencies": {
+        "escape-string-regexp": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+          "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="
+        },
+        "source-map": {
+          "version": "0.5.7",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+          "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ=="
+        }
+      }
+    },
+    "@emotion/cache": {
+      "version": "11.10.5",
+      "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.10.5.tgz",
+      "integrity": "sha512-dGYHWyzTdmK+f2+EnIGBpkz1lKc4Zbj2KHd4cX3Wi8/OWr5pKslNjc3yABKH4adRGCvSX4VDC0i04mrrq0aiRA==",
+      "requires": {
+        "@emotion/memoize": "^0.8.0",
+        "@emotion/sheet": "^1.2.1",
+        "@emotion/utils": "^1.2.0",
+        "@emotion/weak-memoize": "^0.3.0",
+        "stylis": "4.1.3"
+      }
+    },
+    "@emotion/hash": {
+      "version": "0.9.0",
+      "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.0.tgz",
+      "integrity": "sha512-14FtKiHhy2QoPIzdTcvh//8OyBlknNs2nXRwIhG904opCby3l+9Xaf/wuPvICBF0rc1ZCNBd3nKe9cd2mecVkQ=="
+    },
+    "@emotion/is-prop-valid": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.0.tgz",
+      "integrity": "sha512-3aDpDprjM0AwaxGE09bOPkNxHpBd+kA6jty3RnaEXdweX1DF1U3VQpPYb0g1IStAuK7SVQ1cy+bNBBKp4W3Fjg==",
+      "requires": {
+        "@emotion/memoize": "^0.8.0"
+      }
+    },
+    "@emotion/memoize": {
+      "version": "0.8.0",
+      "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.0.tgz",
+      "integrity": "sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA=="
+    },
+    "@emotion/react": {
+      "version": "11.10.6",
+      "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.10.6.tgz",
+      "integrity": "sha512-6HT8jBmcSkfzO7mc+N1L9uwvOnlcGoix8Zn7srt+9ga0MjREo6lRpuVX0kzo6Jp6oTqDhREOFsygN6Ew4fEQbw==",
+      "requires": {
+        "@babel/runtime": "^7.18.3",
+        "@emotion/babel-plugin": "^11.10.6",
+        "@emotion/cache": "^11.10.5",
+        "@emotion/serialize": "^1.1.1",
+        "@emotion/use-insertion-effect-with-fallbacks": "^1.0.0",
+        "@emotion/utils": "^1.2.0",
+        "@emotion/weak-memoize": "^0.3.0",
+        "hoist-non-react-statics": "^3.3.1"
+      }
+    },
+    "@emotion/serialize": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.1.tgz",
+      "integrity": "sha512-Zl/0LFggN7+L1liljxXdsVSVlg6E/Z/olVWpfxUTxOAmi8NU7YoeWeLfi1RmnB2TATHoaWwIBRoL+FvAJiTUQA==",
+      "requires": {
+        "@emotion/hash": "^0.9.0",
+        "@emotion/memoize": "^0.8.0",
+        "@emotion/unitless": "^0.8.0",
+        "@emotion/utils": "^1.2.0",
+        "csstype": "^3.0.2"
+      }
+    },
+    "@emotion/sheet": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.1.tgz",
+      "integrity": "sha512-zxRBwl93sHMsOj4zs+OslQKg/uhF38MB+OMKoCrVuS0nyTkqnau+BM3WGEoOptg9Oz45T/aIGs1qbVAsEFo3nA=="
+    },
+    "@emotion/styled": {
+      "version": "11.10.6",
+      "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.10.6.tgz",
+      "integrity": "sha512-OXtBzOmDSJo5Q0AFemHCfl+bUueT8BIcPSxu0EGTpGk6DmI5dnhSzQANm1e1ze0YZL7TDyAyy6s/b/zmGOS3Og==",
+      "requires": {
+        "@babel/runtime": "^7.18.3",
+        "@emotion/babel-plugin": "^11.10.6",
+        "@emotion/is-prop-valid": "^1.2.0",
+        "@emotion/serialize": "^1.1.1",
+        "@emotion/use-insertion-effect-with-fallbacks": "^1.0.0",
+        "@emotion/utils": "^1.2.0"
+      }
+    },
+    "@emotion/unitless": {
+      "version": "0.8.0",
+      "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.0.tgz",
+      "integrity": "sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw=="
+    },
+    "@emotion/use-insertion-effect-with-fallbacks": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.0.tgz",
+      "integrity": "sha512-1eEgUGmkaljiBnRMTdksDV1W4kUnmwgp7X9G8B++9GYwl1lUdqSndSriIrTJ0N7LQaoauY9JJ2yhiOYK5+NI4A==",
+      "requires": {}
+    },
+    "@emotion/utils": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.0.tgz",
+      "integrity": "sha512-sn3WH53Kzpw8oQ5mgMmIzzyAaH2ZqFEbozVVBSYp538E06OSE6ytOp7pRAjNQR+Q/orwqdQYJSe2m3hCOeznkw=="
+    },
+    "@emotion/weak-memoize": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.0.tgz",
+      "integrity": "sha512-AHPmaAx+RYfZz0eYu6Gviiagpmiyw98ySSlQvCUhVGDRtDFe4DBS0x1bSjdF3gqUDYOczB+yYvBTtEylYSdRhg=="
+    },
     "@eslint/eslintrc": {
       "version": "1.4.1",
       "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.4.1.tgz",
@@ -19342,6 +19976,128 @@
       "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz",
       "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A=="
     },
+    "@mui/base": {
+      "version": "5.0.0-alpha.119",
+      "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-alpha.119.tgz",
+      "integrity": "sha512-XA5zhlYfXi67u613eIF0xRmktkatx6ERy3h+PwrMN5IcWFbgiL1guz8VpdXON+GWb8+G7B8t5oqTFIaCqaSAeA==",
+      "requires": {
+        "@babel/runtime": "^7.21.0",
+        "@emotion/is-prop-valid": "^1.2.0",
+        "@mui/types": "^7.2.3",
+        "@mui/utils": "^5.11.11",
+        "@popperjs/core": "^2.11.6",
+        "clsx": "^1.2.1",
+        "prop-types": "^15.8.1",
+        "react-is": "^18.2.0"
+      },
+      "dependencies": {
+        "react-is": {
+          "version": "18.2.0",
+          "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
+          "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w=="
+        }
+      }
+    },
+    "@mui/core-downloads-tracker": {
+      "version": "5.11.11",
+      "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.11.11.tgz",
+      "integrity": "sha512-0YK0K9GfW1ysw9z4ztWAjLW+bktf+nExMyn2+EQe1Ijb0kF2kz1kIOmb4+di0/PsXG70uCuw4DhEIdNd+JQkRA=="
+    },
+    "@mui/icons-material": {
+      "version": "5.11.11",
+      "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.11.11.tgz",
+      "integrity": "sha512-Eell3ADmQVE8HOpt/LZ3zIma8JSvPh3XgnhwZLT0k5HRqZcd6F/QDHc7xsWtgz09t+UEFvOYJXjtrwKmLdwwpw==",
+      "requires": {
+        "@babel/runtime": "^7.21.0"
+      }
+    },
+    "@mui/material": {
+      "version": "5.11.11",
+      "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.11.11.tgz",
+      "integrity": "sha512-sSe0dmKjB1IGOYt32Pcha+cXV3IIrX5L5mFAF9LDRssp/x53bluhgLLbkc8eTiJvueVvo6HAyze6EkFEYLQRXQ==",
+      "requires": {
+        "@babel/runtime": "^7.21.0",
+        "@mui/base": "5.0.0-alpha.119",
+        "@mui/core-downloads-tracker": "^5.11.11",
+        "@mui/system": "^5.11.11",
+        "@mui/types": "^7.2.3",
+        "@mui/utils": "^5.11.11",
+        "@types/react-transition-group": "^4.4.5",
+        "clsx": "^1.2.1",
+        "csstype": "^3.1.1",
+        "prop-types": "^15.8.1",
+        "react-is": "^18.2.0",
+        "react-transition-group": "^4.4.5"
+      },
+      "dependencies": {
+        "react-is": {
+          "version": "18.2.0",
+          "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
+          "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w=="
+        }
+      }
+    },
+    "@mui/private-theming": {
+      "version": "5.11.11",
+      "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.11.11.tgz",
+      "integrity": "sha512-yLgTkjNC1mpye2SOUkc+zQQczUpg8NvQAETvxwXTMzNgJK1pv4htL7IvBM5vmCKG7IHAB3hX26W2u6i7bxwF3A==",
+      "requires": {
+        "@babel/runtime": "^7.21.0",
+        "@mui/utils": "^5.11.11",
+        "prop-types": "^15.8.1"
+      }
+    },
+    "@mui/styled-engine": {
+      "version": "5.11.11",
+      "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.11.11.tgz",
+      "integrity": "sha512-wV0UgW4lN5FkDBXefN8eTYeuE9sjyQdg5h94vtwZCUamGQEzmCOtir4AakgmbWMy0x8OLjdEUESn9wnf5J9MOg==",
+      "requires": {
+        "@babel/runtime": "^7.21.0",
+        "@emotion/cache": "^11.10.5",
+        "csstype": "^3.1.1",
+        "prop-types": "^15.8.1"
+      }
+    },
+    "@mui/system": {
+      "version": "5.11.11",
+      "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.11.11.tgz",
+      "integrity": "sha512-a9gaOAJBjpzypDfhbGZQ8HzdcxdxsKkFvbp1aAWZhFHBPdehEkARNh7mj851VfEhD/GdffYt85PFKFKdUta5Eg==",
+      "requires": {
+        "@babel/runtime": "^7.21.0",
+        "@mui/private-theming": "^5.11.11",
+        "@mui/styled-engine": "^5.11.11",
+        "@mui/types": "^7.2.3",
+        "@mui/utils": "^5.11.11",
+        "clsx": "^1.2.1",
+        "csstype": "^3.1.1",
+        "prop-types": "^15.8.1"
+      }
+    },
+    "@mui/types": {
+      "version": "7.2.3",
+      "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.3.tgz",
+      "integrity": "sha512-tZ+CQggbe9Ol7e/Fs5RcKwg/woU+o8DCtOnccX6KmbBc7YrfqMYEYuaIcXHuhpT880QwNkZZ3wQwvtlDFA2yOw==",
+      "requires": {}
+    },
+    "@mui/utils": {
+      "version": "5.11.11",
+      "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.11.11.tgz",
+      "integrity": "sha512-neMM5rrEXYQrOrlxUfns/TGgX4viS8K2zb9pbQh11/oUUYFlGI32Tn+PHePQx7n6Fy/0zq6WxdBFC9VpnJ5JrQ==",
+      "requires": {
+        "@babel/runtime": "^7.21.0",
+        "@types/prop-types": "^15.7.5",
+        "@types/react-is": "^16.7.1 || ^17.0.0",
+        "prop-types": "^15.8.1",
+        "react-is": "^18.2.0"
+      },
+      "dependencies": {
+        "react-is": {
+          "version": "18.2.0",
+          "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
+          "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w=="
+        }
+      }
+    },
     "@nicolo-ribaudo/eslint-scope-5-internals": {
       "version": "5.1.1-v1",
       "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz",
@@ -19405,6 +20161,22 @@
         "source-map": "^0.7.3"
       }
     },
+    "@popperjs/core": {
+      "version": "2.11.6",
+      "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz",
+      "integrity": "sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw=="
+    },
+    "@reduxjs/toolkit": {
+      "version": "1.9.3",
+      "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.3.tgz",
+      "integrity": "sha512-GU2TNBQVofL09VGmuSioNPQIu6Ml0YLf4EJhgj0AvBadRlCGzUWet8372LjvO4fqKZF2vH1xU0htAa7BrK9pZg==",
+      "requires": {
+        "immer": "^9.0.16",
+        "redux": "^4.2.0",
+        "redux-thunk": "^2.4.2",
+        "reselect": "^4.1.7"
+      }
+    },
     "@remix-run/router": {
       "version": "1.3.3",
       "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.3.3.tgz",
@@ -20242,6 +21014,22 @@
         "@types/react": "*"
       }
     },
+    "@types/react-is": {
+      "version": "17.0.3",
+      "resolved": "https://registry.npmjs.org/@types/react-is/-/react-is-17.0.3.tgz",
+      "integrity": "sha512-aBTIWg1emtu95bLTLx0cpkxwGW3ueZv71nE2YFBpL8k/z5czEW8yYpOo8Dp+UUAFAtKwNaOsh/ioSeQnWlZcfw==",
+      "requires": {
+        "@types/react": "*"
+      }
+    },
+    "@types/react-transition-group": {
+      "version": "4.4.5",
+      "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.5.tgz",
+      "integrity": "sha512-juKD/eiSM3/xZYzjuzH6ZwpP+/lejltmiS3QEzV/vmb/Q8+HfDmxu+Baga8UEMGBqV88Nbg4l2hY/K2DkyaLLA==",
+      "requires": {
+        "@types/react": "*"
+      }
+    },
     "@types/resolve": {
       "version": "1.17.1",
       "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz",
@@ -21410,6 +22198,11 @@
         "wrap-ansi": "^7.0.0"
       }
     },
+    "clsx": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
+      "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg=="
+    },
     "co": {
       "version": "4.6.0",
       "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
@@ -22083,6 +22876,15 @@
         "utila": "~0.4"
       }
     },
+    "dom-helpers": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
+      "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
+      "requires": {
+        "@babel/runtime": "^7.8.7",
+        "csstype": "^3.0.2"
+      }
+    },
     "dom-serializer": {
       "version": "1.4.1",
       "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz",
@@ -23129,6 +23931,11 @@
         "pkg-dir": "^4.1.0"
       }
     },
+    "find-root": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
+      "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng=="
+    },
     "find-up": {
       "version": "5.0.0",
       "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
@@ -27545,6 +28352,17 @@
         "workbox-webpack-plugin": "^6.4.1"
       }
     },
+    "react-transition-group": {
+      "version": "4.4.5",
+      "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
+      "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
+      "requires": {
+        "@babel/runtime": "^7.5.5",
+        "dom-helpers": "^5.0.1",
+        "loose-envify": "^1.4.0",
+        "prop-types": "^15.6.2"
+      }
+    },
     "read-cache": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@@ -27708,6 +28526,11 @@
       "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
       "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="
     },
+    "reselect": {
+      "version": "4.1.7",
+      "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.7.tgz",
+      "integrity": "sha512-Zu1xbUt3/OPwsXL46hvOOoQrap2azE7ZQbokq61BQfiXvhewsKDwhMeZjTX9sX0nvw1t/U5Audyn1I9P/m9z0A=="
+    },
     "resolve": {
       "version": "1.22.1",
       "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz",
@@ -28386,6 +29209,11 @@
         "postcss-selector-parser": "^6.0.4"
       }
     },
+    "stylis": {
+      "version": "4.1.3",
+      "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.1.3.tgz",
+      "integrity": "sha512-GP6WDNWf+o403jrEp9c5jibKavrtLW+/qYGhFxFrG8maXhwTBI7gLLhiBb0o7uFccWN+EOS9aMO6cGHWAO07OA=="
+    },
     "supports-color": {
       "version": "5.5.0",
       "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",

+ 5 - 0
js21 react/my-react-app/package.json

@@ -3,6 +3,11 @@
   "version": "0.1.0",
   "private": true,
   "dependencies": {
+    "@emotion/react": "^11.10.6",
+    "@emotion/styled": "^11.10.6",
+    "@mui/icons-material": "^5.11.11",
+    "@mui/material": "^5.11.11",
+    "@reduxjs/toolkit": "^1.9.3",
     "@testing-library/jest-dom": "^5.16.5",
     "@testing-library/react": "^13.4.0",
     "@testing-library/user-event": "^13.5.0",

+ 5 - 4
js21 react/my-react-app/src/App.js

@@ -9,15 +9,16 @@ import Register from './pages/Register';
 import Login from './pages/Login';
 import Admin from './pages/Admin';
 import Cart from './pages/Cart';
-import {ConnectCategory} from './pages/Category';
+import {Category} from './pages/Category/Category';
 import Good from './pages/Good';
 import Layout from './components/Layout/Layout';
 import store from './redux/reducers';
+import './App.scss'
 
 
 function App() {
   return (
-    <div>
+    <div className='app'>
       <Provider store={store}>
         <Routes>
           <Route path='/' element={<Layout />}>
@@ -27,8 +28,8 @@ function App() {
             <Route path='/admin' element={<Admin />} />
             <Route path='/register' element={<Register />} />
             <Route path='/login' element={<Login />} />
-            <Route path='/:category/:catId' element={<ConnectCategory />} />
-            <Route path='/:goodName/:goodId' element={<Good />} />
+            <Route path='/:category/:categoryId' element={<Category />} />
+            <Route path='/good/:goodId' element={<Good />} />
             <Route path='*' element={<NotFound />} />
           </Route>
         </Routes>

+ 172 - 166
js21 react/my-react-app/src/App.scss

@@ -1,172 +1,178 @@
-.active {
-  color: rgb(18, 139, 199);
-}
-
-#main {
-  max-width: 1000px;
-  width: 100%;
-}
-
-.good-cart .image-container {
-  cursor: pointer;
-}
-
-.icon-container {
-  width: 20px;
-  text-align: center;
-  height: 20px;
-  padding: 0;
-  border: none;
-  background: none;
-  cursor: pointer;
-}
-
-.count-container {
-  display: flex;
-  justify-content: flex-end;
-}
-
-.count-container > * {
-  margin: 8px;
-}
-
-.count-container input,
-.good-cart input,
-.good-section input {
-  max-width: 50px;
-  width: 100%;
-  text-align: center;
-  padding: 4px 0;
-}
-
-input[type="number"]::-webkit-outer-spin-button,
-input[type="number"]::-webkit-inner-spin-button {
-  -webkit-appearance: none;
-}
-
-.icon-container img {
-  width: 100%;
-  height: 100%;
-}
-.goods-container {
-  display: flex;
-  flex-wrap: wrap;
-  justify-content: center;
-  gap: 24px 12px;
-  margin-top: 20px;
-}
-
-.good-cart {
-  flex-basis: 270px;
-  border-radius: 16px;
-  padding: 16px;
-  position: relative;
-  box-shadow: 0px 0px 13px -8px rgba(66, 68, 90, 1);
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-}
-
-.good-cart > * {
-  margin: 6px;
-}
-
-.good-cart > a {
-  text-decoration: none;
-  text-align: center;
-  color: rgba(66, 68, 90, 1);
-  font-weight: 300;
-  line-height: 20px;
-  height: 40px;
-}
-
-
-.goods-out-stock,
-.cart-is-empty {
-  font-size: 24px;
-  text-align: center;
-  font-weight: 300;
-  margin: 20px 0;
-}
-
-.good-section {
-  display: flex;
-  margin: 20px 0;
-}
-
-.good-section > * {
-  width: 50%;
-}
-
-.good-section .image-container {
-  width: 100%;
-  max-height: 400px;
-  height: 100%;
-}
-
-.good-info {
-  padding: 20px;
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-}
-
-.good-info input {
-  margin: 20px 0;
-}
-
-.good-info .buttons-container {
-  text-align: center;
-}
-
-.good-info .buttons-container :first-child {
-  margin-bottom: 20px;
-}
-
-.description {
-  font-weight: 300;
-  margin-bottom: 20px;
-}
-
-.order-container {
-  border-radius: 10px;
-  box-shadow: 0px 0px 23px -8px rgb(155, 157, 176);
-  padding: 20px;
-  margin: 20px 0;
-  font-weight: 300;
-}
-
-.order-container > * {
-  margin: 4px 0;
-}
-
-.right-aligment {
-  text-align: end;
-}
-
-.order-button {
-  margin-top: 20px;
-  float: right;
-}
-
-.form {
-  border: 1px solid gray;
+.app {
+  height: 100vh;
   display: flex;
   flex-direction: column;
-  align-items: center;
-  border-radius: 10px;
-  max-width: 300px;
-  width: 100%;
-  margin: 100px auto 0;
-  padding: 20px;
 }
 
-.form > * {
-  margin: 4px;
+.active {
+  color: rgb(18, 139, 199);
 }
 
-.warning-message {
-  margin-top: 70px;
-  display: none;
-  font-style: italic;
-}
+// #main {
+//   max-width: 1000px;
+//   width: 100%;
+// }
+
+// .good-cart .image-container {
+//   cursor: pointer;
+// }
+
+// .icon-container {
+//   width: 20px;
+//   text-align: center;
+//   height: 20px;
+//   padding: 0;
+//   border: none;
+//   background: none;
+//   cursor: pointer;
+// }
+
+// .count-container {
+//   display: flex;
+//   justify-content: flex-end;
+// }
+
+// .count-container > * {
+//   margin: 8px;
+// }
+
+// .count-container input,
+// .good-cart input,
+// .good-section input {
+//   max-width: 50px;
+//   width: 100%;
+//   text-align: center;
+//   padding: 4px 0;
+// }
+
+// input[type="number"]::-webkit-outer-spin-button,
+// input[type="number"]::-webkit-inner-spin-button {
+//   -webkit-appearance: none;
+// }
+
+// .icon-container img {
+//   width: 100%;
+//   height: 100%;
+// }
+// .goods-container {
+//   display: flex;
+//   flex-wrap: wrap;
+//   justify-content: center;
+//   gap: 24px 12px;
+//   margin-top: 20px;
+// }
+
+// .good-cart {
+//   flex-basis: 270px;
+//   border-radius: 16px;
+//   padding: 16px;
+//   position: relative;
+//   box-shadow: 0px 0px 13px -8px rgba(66, 68, 90, 1);
+//   display: flex;
+//   flex-direction: column;
+//   align-items: center;
+// }
+
+// .good-cart > * {
+//   margin: 6px;
+// }
+
+// .good-cart > a {
+//   text-decoration: none;
+//   text-align: center;
+//   color: rgba(66, 68, 90, 1);
+//   font-weight: 300;
+//   line-height: 20px;
+//   height: 40px;
+// }
+
+
+// .goods-out-stock,
+// .cart-is-empty {
+//   font-size: 24px;
+//   text-align: center;
+//   font-weight: 300;
+//   margin: 20px 0;
+// }
+
+// .good-section {
+//   display: flex;
+//   margin: 20px 0;
+// }
+
+// .good-section > * {
+//   width: 50%;
+// }
+
+// .good-section .image-container {
+//   width: 100%;
+//   max-height: 400px;
+//   height: 100%;
+// }
+
+// .good-info {
+//   padding: 20px;
+//   display: flex;
+//   flex-direction: column;
+//   align-items: center;
+// }
+
+// .good-info input {
+//   margin: 20px 0;
+// }
+
+// .good-info .buttons-container {
+//   text-align: center;
+// }
+
+// .good-info .buttons-container :first-child {
+//   margin-bottom: 20px;
+// }
+
+// .description {
+//   font-weight: 300;
+//   margin-bottom: 20px;
+// }
+
+// .order-container {
+//   border-radius: 10px;
+//   box-shadow: 0px 0px 23px -8px rgb(155, 157, 176);
+//   padding: 20px;
+//   margin: 20px 0;
+//   font-weight: 300;
+// }
+
+// .order-container > * {
+//   margin: 4px 0;
+// }
+
+// .right-aligment {
+//   text-align: end;
+// }
+
+// .order-button {
+//   margin-top: 20px;
+//   float: right;
+// }
+
+// .form {
+//   border: 1px solid gray;
+//   display: flex;
+//   flex-direction: column;
+//   align-items: center;
+//   border-radius: 10px;
+//   max-width: 300px;
+//   width: 100%;
+//   margin: 100px auto 0;
+//   padding: 20px;
+// }
+
+// .form > * {
+//   margin: 4px;
+// }
+
+// .warning-message {
+//   margin-top: 70px;
+//   display: none;
+//   font-style: italic;
+// }

+ 150 - 0
js21 react/my-react-app/src/api/gql.js

@@ -0,0 +1,150 @@
+const API_URL = "http://shop-roles.node.ed.asmer.org.ua/graphql";
+
+function getGql(url) {
+  let headers = {
+    'Content-Type': 'application/json;charset=utf-8',
+    'Accept': 'application/json'
+  }
+  return (query, variables = {}) => {
+    if ("authToken" in localStorage) {
+      headers.Authorization = "Bearer " + localStorage.authToken;
+    }
+    return fetch(url, {
+      method: 'POST',
+      headers,
+      body: JSON.stringify({
+        query,
+        variables
+      }),
+    }).then(response => response.json()).then(response => {
+      if (response.data) {
+        return Object.values(response.data)[0];
+      } else if (response.errors) {
+        throw new Error(JSON.stringify(response.errors));
+      }
+    });
+  }
+}
+
+const gql = getGql(API_URL);
+
+
+export const gqlGetCategories = () => {
+  const categoriesQuery = `query categories($q: String){
+    CategoryFind(query: $q){
+      _id
+      name,
+      goods{
+        name
+      },
+      parent{
+        name
+      },
+      image{
+        url
+      },
+      subCategories{
+        name,
+        subCategories{
+          name
+        }
+      }
+    }
+}`;
+  return gql(categoriesQuery, { q: "[{\"parent\": null}]" });
+}
+
+export const gqlGetCategory = (id) => {
+  const categoryQuery = `query category($q: String) {
+    CategoryFindOne(query: $q) {
+      _id
+      name,
+      goods{
+        name,
+        _id,
+        images{
+          _id,
+          url
+        },
+        price
+      },
+      parent {
+        _id,
+        name
+      },
+      subCategories{
+        name,
+        _id
+        subCategories{
+          name,
+          _id
+        }
+      }
+    }
+}`;
+  return gql(categoryQuery, { q: `[{"_id": "${id}"}]` });
+}
+
+// const gqlGetGood = (id) => {
+//   const goodQuery = `query good($q: String) {
+//     GoodFindOne(query: $q) {
+//       _id,
+//       name,
+//       categories{
+//         _id,
+//         name
+//       },
+//       description,
+//       price,
+//       images{
+//         _id,
+//         url
+//       }
+//     }
+// }`;
+//   return gql(goodQuery, { q: `[{"_id": "${id}"}]` });
+// }
+
+// const gqlLogin = (login, password) => {
+//   const loginQuery = `query login($login:String, $password:String){
+//     login(login:$login, password:$password)
+// }`;
+//   return gql(loginQuery, { login, password });
+// }
+
+// const gqlCreateUser = (login, password) => {
+//   const registrationQuery = `mutation registration($login:String, $password: String){
+//     UserUpsert(user: {login:$login, password: $password}){
+//         _id login createdAt
+//     }
+// }`;
+//   return gql(registrationQuery, { login, password });
+// }
+
+// const gqlGetOwnerOrders = () => {
+//   const ordersQuery = `query orders($q: String) {
+//     OrderFind(query: $q) {
+//       _id, total, createdAt, owner{
+//         _id, login
+//       }, orderGoods{
+//         price, count, good{
+//           name
+//         }
+//       }
+//     }
+//   }`;
+//   return gql(ordersQuery, { q: `[{}]` });
+// }
+
+// const gqlCreateOrder = (orderGoods) => {
+//   const orderQuery = `mutation ordering($orderGoods: OrderInput) {
+//     OrderUpsert(order: $orderGoods) {
+//       _id, total, orderGoods {
+//         good {
+//           name
+//         }
+//       }
+//     }
+//   }`;
+//   return gql(orderQuery, { orderGoods: { orderGoods } });
+// }

+ 4 - 4
js21 react/my-react-app/src/components/Button/Button.js

@@ -1,8 +1,8 @@
 import React from "react";
-import './Button.scss';
+import Button from '@mui/material/Button';
 
-export function Button(children) {
+export default function ButtonEl({children}) {
   return (
-    <button className="button">{children}</button>
-  )
+      <Button variant="contained">{children}</Button>
+  );
 }

+ 0 - 14
js21 react/my-react-app/src/components/Button/Button.scss

@@ -1,14 +0,0 @@
-.button {
-  border: none;
-  color: #fff;
-  background-color: rgb(22, 29, 168);
-  padding: 8px 10px;
-  border-radius: 6px;
-  max-width: 140px;
-  width: 100%;
-  font-size: 16px;
-
-  .button:hover {
-    color: rgb(233, 233, 73);
-  }
-}

+ 19 - 8
js21 react/my-react-app/src/components/Categories/CategoryMenu.js

@@ -1,16 +1,27 @@
-import React from 'react';
+import React, { useEffect } from 'react';
 import { Link } from 'react-router-dom';
-import { connect, useDispatch, useSelector } from 'react-redux';
-
+import { useDispatch, useSelector } from 'react-redux';
 import './CategoryMenu.scss'
+import { fetchCategories, selectCategories, selectStatus } from '../../redux/slices/categoriesSlice';
+import Skeleton from '@mui/material/Skeleton';
+
+
+export const CategoryMenu = () => {
+const dispatch = useDispatch();
+useEffect(()=> {
+  dispatch(fetchCategories());
+}, [dispatch])
+
+
 
+const categories = useSelector(selectCategories);
+const status = useSelector(selectStatus);
+console.log('categories', categories);
 
-const CategoryMenu = ({ categories = [] }) => {
   return (
     <aside className='aside'>
-      {categories.map(category => <Link key={category._id} to={`/${category.name}/${category._id}`}>{category.name}</Link>)}
+       {status === 'loading' && Array(10).fill(1).map(item => <Skeleton className='skeleton'/> )}
+       {status === 'succeeded' && categories?.map(category => <Link key={category._id} to={`/${category.name}/${category._id}`}>{category.name}</Link>)}
     </aside>
   )
-}
-const ReduxCategoryMenu = connect(state => ({ categories: state.promise.categories?.payload }))(CategoryMenu);
-export {ReduxCategoryMenu};
+}

+ 5 - 0
js21 react/my-react-app/src/components/Categories/CategoryMenu.scss

@@ -14,4 +14,9 @@
     color: gray;
     transition: color 0.3s;
   }
+  .skeleton {
+    padding: 10px 20px;
+    margin-bottom: 8px;
+    height: 40px;
+  }
 }

+ 2 - 2
js21 react/my-react-app/src/components/CategoriesSection/CategoriesSection.js

@@ -4,9 +4,9 @@ import './CategoriesSection.scss';
 
 export function CategoriesSection({categories, categoryEl}) {
   return (
-    <section className="'info-about-category'">
+    <section className="info-about-category">
       {categoryEl}
-      {categories.map(category => <Link key={category.id} to={`/${category.name}/${category._id}`} className="'link-style'">
+      {categories?.map(category => <Link key={category.id} to={`/${category.name}/${category._id}`} className="link-style">
                                         {category.name}</Link>)}
     </section>
   )

+ 3 - 5
js21 react/my-react-app/src/components/CategoriesSection/CategoriesSection.scss

@@ -1,15 +1,13 @@
 .info-about-category {
-  * {
-    margin: 0 8px;
-    font-weight: 300;
-  }
+  text-align: left;
 
   .link-style {
     color: rgb(18, 139, 199);
+    margin: 0 6px;
   }
 
   .link-style:hover {
     color: rgb(255, 42, 42);
-    transition: color 0.3s;
+    transition: color 0.4s;
   }
 }

+ 0 - 8
js21 react/my-react-app/src/components/Caunter/Caunter.js

@@ -1,8 +0,0 @@
-import React from "react";
-import './styles.scss';
-
-export function Counter({value}) {
-  return (
-    <input type='number' min='1' value={value}/>
-  )
-}

+ 17 - 0
js21 react/my-react-app/src/components/Counter/Counter.js

@@ -0,0 +1,17 @@
+import React from "react";
+import TextField from '@mui/material/TextField';
+import Box from '@mui/material/Box';
+import './Counter.scss'
+
+export function Counter({ value }) {
+  return (
+    <Box sx={{ width: '50px'}}>
+      <TextField
+        type="number"
+        min="1"
+        value={value}
+        size="small"
+      />
+    </Box>
+  )
+}

+ 7 - 0
js21 react/my-react-app/src/components/Counter/Counter.scss

@@ -0,0 +1,7 @@
+input {
+  text-align: center;
+}
+input[type="number"]::-webkit-outer-spin-button,
+input[type="number"]::-webkit-inner-spin-button {
+  -webkit-appearance: none;
+}

+ 25 - 0
js21 react/my-react-app/src/components/GoodCart/GoodCart.js

@@ -0,0 +1,25 @@
+import Card from '@mui/material/Card';
+import CardActions from '@mui/material/CardActions';
+import CardContent from '@mui/material/CardContent';
+import { Link } from 'react-router-dom';
+import './GoodCart.scss'
+import { Image } from '../Image/Image';
+import { Price } from '../Price/Price';
+import { Counter } from '../Counter/Counter.js';
+import ButtonEl from '../Button/Button';
+
+
+
+export default function GoodCart({ good }) {
+  return (
+    <Card sx={{ width: 270 }}>
+      <CardContent className='good-cart'>
+        <Link to={`/good/${good?._id}`} className='good-name'>{good?.name}</Link>
+        <Link to={`/good/${good?._id}`}><Image url={`${good?.images[0]?.url}`} /></Link>
+        <Price>{good?.price}</Price>
+        <Counter value={1} />
+        <ButtonEl>Add to cart</ButtonEl>
+      </CardContent>
+    </Card>
+  )
+}

+ 17 - 0
js21 react/my-react-app/src/components/GoodCart/GoodCart.scss

@@ -0,0 +1,17 @@
+.good-cart {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  position: relative;
+  * {
+    margin: 4px 0;
+  }
+  .good-name {
+    text-decoration: none;
+    color: rgba(66, 68, 90, 1);
+    font-weight: 300;
+    line-height: 20px;
+    height: 48px;
+    text-align: center;
+  }
+}

+ 22 - 7
js21 react/my-react-app/src/components/Header/Header.js

@@ -1,28 +1,43 @@
 import React from 'react';
-import { connect } from 'react-redux';
 import { NavLink } from 'react-router-dom';
 import { authReducer } from '../../redux/reducers/authReducer';
 import './Header.scss';
+import DiscountIcon from '@mui/icons-material/Discount';
+import Badge from '@mui/material/Badge';
+import { styled } from '@mui/material/styles';
+import IconButton from '@mui/material/IconButton';
+import ShoppingCartIcon from '@mui/icons-material/ShoppingCart';
 
-const UserName = ({userName}) => <div>{userName || 'anon'}</div>
+// const UserName = ({userName}) => <div>{userName || 'anon'}</div>
 
-const ConnectUserName = connect(state => ({userName: state.auth.payload?.sub?.login}))(UserName);
+// const ConnectUserName = connect(state => ({userName: state.auth.payload?.sub?.login}))(UserName);
 
+const StyledBadge = styled(Badge)(({ theme }) => ({
+  '& .MuiBadge-badge': {
+
+    top: 24,
+    border: `2px solid ${theme.palette.background.paper}`,
+    padding: '0 4px',
+  },
+}));
 export default function Header() {
   return (
     <header>
-      <NavLink to='/'><img src="/images/logo.png" alt="" /></NavLink>
+      <NavLink to='/' className='logo'><DiscountIcon sx={{ fontSize: 40 }} /><span>KAPRAN Co.</span></NavLink>
       <div className="header-info">
         <div id="historyPage">
           <NavLink to="history">My orders</NavLink>
         </div>
         <div className="cart-section">
           <NavLink to="cart">
-          <img className="cart-icon" src="/images/shopping-cart.png" />
-          <div id='cartIconEl' >0</div>
+            <IconButton aria-label="cart">
+              <StyledBadge badgeContent={1} color="primary">
+                <ShoppingCartIcon sx={{color: 'white', fontSize: '30px'}}/>
+              </StyledBadge>
+            </IconButton>
           </NavLink>
         </div>
-        <ConnectUserName />
+        {/* <ConnectUserName /> */}
         <div className="login-block">
           <NavLink to="login" id="login" className="button">Login</NavLink>
           <NavLink to="register" id="registration" className="button">Registration</NavLink>

+ 32 - 34
js21 react/my-react-app/src/components/Header/Header.scss

@@ -1,7 +1,6 @@
 header {
   min-height: 100px;
-  height: 100%;
-  background-color: rgb(37, 44, 177);
+  background-color: #1976d2;
   color: #fff;
   display: flex;
   justify-content: space-between;
@@ -14,52 +13,51 @@ header {
     border-radius: 10px;
     padding: 6px 12px;
     cursor: pointer;
-    color: rgb(37, 44, 177);
+    color: #1976d2;
     font-size: 16px;
     transition: color 0.3s;
   }
-
-a {
+  .logo {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+  }
+  a {
     color: #fff;
     text-decoration: none;
     transition: color 0.3s;
-    img {
-      display: flex;
-    }
   }
 
   .button:hover,
   a:hover {
     color: rgb(165, 189, 251);
   }
-}
-
-.header-info {
-  display: flex;
-  align-items: center;
-  & > div {
-    margin: 0 14px;
-  }
-}
-
-.cart-section {
-  position: relative;
-  .cart-icon {
-    width: 34px;
-    cursor: pointer;
+  .header-info {
+    display: flex;
+    align-items: center;
+    & > div {
+      margin: 0 14px;
+    }
   }
-  #cartIconEl {
-    border-radius: 50%;
-    background-color: rgb(158, 205, 255);
-    padding: 1px 6px;
-    font-size: 14px;
-    position: absolute;
-    transform: translate(28px, -16px);
+  .cart-section {
+    position: relative;
+    .cart-icon {
+      width: 34px;
+      cursor: pointer;
+    }
+    #cartIconEl {
+      border-radius: 50%;
+      background-color: rgb(158, 205, 255);
+      padding: 1px 6px;
+      font-size: 14px;
+      position: absolute;
+      transform: translate(28px, -16px);
+    }
   }
-}
 
-.login-block {
-  .button:first-child {
-    margin-right: 8px;
+  .login-block {
+    .button:first-child {
+      margin-right: 8px;
+    }
   }
 }

+ 1 - 5
js21 react/my-react-app/src/components/Image/Image.js

@@ -1,15 +1,11 @@
 import React from "react";
 import './Image.scss';
 
-export function Image({url, id}) {
+export function Image({url}) {
 
   return (
     <div className="image-container">
       <img className="good-image" src={`http://shop-roles.node.ed.asmer.org.ua/${url}`}/>
     </div>
   )
-
-  // if (route == 'category') {
-  //   imageContainer.addEventListener('click', (e) => location.href = `#/good/${id}`);
-  // }
 }

+ 5 - 2
js21 react/my-react-app/src/components/Layout/Layout.js

@@ -2,7 +2,8 @@ import React from 'react';
 import { Link, Outlet } from 'react-router-dom';
 import Footer from '../Footer';
 import Header from '../Header/Header';
-import {ReduxCategoryMenu} from '../Categories/CategoryMenu';
+import {CategoryMenu, ReduxCategoryMenu} from '../Categories/CategoryMenu';
+import Box from '@mui/material/Box';
 
 
 import './Layout.scss'
@@ -12,8 +13,10 @@ export default function Layout() {
     <>
     <Header />
     <main className='main-container'>
-    <ReduxCategoryMenu />
+    <CategoryMenu />
+    <Box sx={{ margin: '0 auto', width: '100%'}}>
     <Outlet />
+    </Box>
     </main>
     <Footer />
     </>

+ 1 - 0
js21 react/my-react-app/src/components/Layout/Layout.scss

@@ -3,4 +3,5 @@
   position: relative;
   width: 100%;
   padding: 20px;
+  flex-grow: 1;
 }

+ 11 - 0
js21 react/my-react-app/src/components/Loader.js

@@ -0,0 +1,11 @@
+import * as React from 'react';
+import Box from '@mui/material/Box';
+import LinearProgress from '@mui/material/LinearProgress';
+
+export default function Loader() {
+  return (
+    <Box sx={{ width: '70%', margin: '200px auto' }}>
+      <LinearProgress />
+    </Box>
+  );
+}

+ 1 - 1
js21 react/my-react-app/src/components/Price/Price.js

@@ -4,7 +4,7 @@ import './Price.scss';
 export function Price( {children}) {
   return (
     <div className="price">
-      {children}
+      {children} UAH
     </div>
   )
 }

+ 1 - 1
js21 react/my-react-app/src/components/Price/Price.scss

@@ -1,5 +1,5 @@
 .price {
   font-size: 18px;
-  font-weight: 500;
+  font-weight: 400;
   color: rgb(15, 29, 67);
 }

+ 2 - 2
js21 react/my-react-app/src/components/Title/Title.js

@@ -3,9 +3,9 @@ import './Title.scss';
 
 export function Title({children}) {
   return (
-    <div className="title">
+    <h1 className="title">
       {children}
-    </div>
+    </h1>
   )
 
 }

+ 0 - 1
js21 react/my-react-app/src/components/Title/Title.scss

@@ -1,5 +1,4 @@
 .title {
-  font-size: 32px;
   text-align: center;
   font-weight: 300;
   margin: 20px 0;

+ 0 - 16
js21 react/my-react-app/src/functions/styles.scss

@@ -1,16 +0,0 @@
-
-
-.form {
-  border: 1px solid gray;
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  border-radius: 10px;
-  max-width: 300px;
-  width: 100%;
-  margin: 100px auto 0;
-  padding: 20px;
-  * {
-    margin: 4px;
-  }
-}

+ 6 - 5
js21 react/my-react-app/src/index.scss

@@ -1,4 +1,5 @@
-@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@300;400&display=swap');
+@import url("https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap");
+@import url("https://fonts.googleapis.com/css2?family=Material+Icons&display=swap");
 
 * {
   margin: 0;
@@ -9,11 +10,11 @@
 body {
   background-color: #fff;
   color: #222;
-  font-family: 'Roboto', sans-serif;
+  font-family: "Roboto", sans-serif;
   font-weight: 300;
 }
 
-footer{
+footer {
   text-align: center;
-  margin-top: 50px;
-}
+  padding: 10px;
+}

+ 0 - 18
js21 react/my-react-app/src/pages/Category.js

@@ -1,18 +0,0 @@
-import React, { useEffect } from 'react'
-import { useParams } from 'react-router-dom';
-import { connect } from 'react-redux';
-import { actionCategoryById } from '../redux/actions/actions';
-
-const Category = ({ category = {}, loadCatogory }) => {
-  const { catId } = useParams();
-
-  useEffect(() => {
-    loadCatogory(catId)
-  }, [catId]);
-  return (
-    <div>Category {category.name}</div>
-  )
-}
-const ConnectCategory = connect(state => ({category: state.promise.category?.payload}), { loadCatogory: actionCategoryById })(Category);
-
-export { Category, ConnectCategory };

+ 35 - 0
js21 react/my-react-app/src/pages/Category/Category.js

@@ -0,0 +1,35 @@
+import React, { useEffect } from 'react'
+import { useParams } from 'react-router-dom';
+import { useDispatch, useSelector } from 'react-redux';
+import { fetchCategoryById, selectSelectedCategory, selectStatus } from '../../redux/slices/categoriesSlice';
+import { Title } from '../../components/Title/Title';
+import { CategoriesSection } from '../../components/CategoriesSection/CategoriesSection';
+import GoodCart from '../../components/GoodCart/GoodCart';
+import './Category.scss';
+import Loader from '../../components/Loader';
+
+
+export const Category = () => {
+  const { categoryId } = useParams();
+  const dispatch = useDispatch();
+  const status = useSelector(selectStatus);
+  useEffect(() => {
+    dispatch(fetchCategoryById(categoryId));
+  }, [dispatch, categoryId]);
+  const category = useSelector(selectSelectedCategory);
+  console.log('category', category);
+  return (
+    <>
+      {status === 'loading' && <Loader />}
+      {status === 'succeeded' && <>
+        <Title>{category?.name}</Title>
+        {category?.subCategories?.length > 0 && <CategoriesSection categories={category?.subCategories} categoryEl='Subcategories:' />}
+        {/* {category?.parent?.length > 0 && <CategoriesSection categories={category?.parent} categoryEl='Parent Category:'/>} */}
+        {/* спросить Андрея про parent */}
+        <div className='good-container'>
+          {category?.goods?.map(good => <GoodCart good={good} />)}
+        </div>
+      </>}
+    </>
+  )
+}

+ 7 - 0
js21 react/my-react-app/src/pages/Category/Category.scss

@@ -0,0 +1,7 @@
+.good-container {
+  display: flex;
+  flex-wrap: wrap;
+  justify-content: center;
+  gap: 24px 12px;
+  margin-top: 20px;
+}

+ 36 - 183
js21 react/my-react-app/src/redux/actions/actions.js

@@ -2,198 +2,51 @@ import { actionPromise } from "../reducers/promiseReducer";
 import { actionAuthLogin } from "../reducers/authReducer";
 import { actionCartClear } from "../reducers/cartReducer";
 
-const API_URL = "http://shop-roles.node.ed.asmer.org.ua/graphql";
 
-function getGql(url) {
-  let headers = {
-    'Content-Type': 'application/json;charset=utf-8',
-    'Accept': 'application/json'
-  }
-  return (query, variables = {}) => {
-    if ("authToken" in localStorage) {
-      headers.Authorization = "Bearer " + localStorage.authToken;
-    }
-    return fetch(url, {
-      method: 'POST',
-      headers,
-      body: JSON.stringify({
-        query,
-        variables
-      }),
-    }).then(response => response.json()).then(response => {
-      if (response.data) {
-        return Object.values(response.data)[0];
-      } else if (response.errors) {
-        throw new Error(JSON.stringify(response.errors));
-      }
-    });
-  }
-}
 
-const gql = getGql(API_URL);
 
-const gqlGetCategories = () => {
-  const categoriesQuery = `query categories($q: String){
-    CategoryFind(query: $q){
-      _id
-      name,
-      goods{
-        name
-      },
-      parent{
-        name
-      },
-      image{
-        url
-      },
-      subCategories{
-        name,
-        subCategories{
-          name
-        }
-      }
-    }
-}`;
-  return gql(categoriesQuery, { q: "[{\"parent\": null}]" });
-}
+// export const actionCategories = () =>
+//   actionPromise('categories', gqlGetCategories());
 
-const gqlGetCategory = (id) => {
-  const categoryQuery = `query category($q: String) {
-    CategoryFindOne(query: $q) {
-      _id
-      name,
-      goods{
-        name,
-        _id,
-        images{
-          _id,
-          url
-        },
-        price
-      },
-      parent {
-        _id,
-        name
-      },
-      subCategories{
-        name,
-        _id
-        subCategories{
-          name,
-          _id
-        }
-      }
-    }
-}`;
-  return gql(categoryQuery, { q: `[{"_id": "${id}"}]` });
-}
+// export const actionCategoryById = (id) =>
+//   actionPromise('category', gqlGetCategory(id));
 
-const gqlGetGood = (id) => {
-  const goodQuery = `query good($q: String) {
-    GoodFindOne(query: $q) {
-      _id,
-      name,
-      categories{
-        _id,
-        name
-      },
-      description,
-      price,
-      images{
-        _id,
-        url
-      }
-    }
-}`;
-  return gql(goodQuery, { q: `[{"_id": "${id}"}]` });
-}
+// export  const actionGoodById = (id) =>
+//   actionPromise('good', gqlGetGood(id));
 
-const gqlLogin = (login, password) => {
-  const loginQuery = `query login($login:String, $password:String){
-    login(login:$login, password:$password)
-}`;
-  return gql(loginQuery, { login, password });
-}
+// export const actionLogin = (login, password) =>
+//   actionPromise('login', gqlLogin(login, password));
 
-const gqlCreateUser = (login, password) => {
-  const registrationQuery = `mutation registration($login:String, $password: String){
-    UserUpsert(user: {login:$login, password: $password}){
-        _id login createdAt
-    }
-}`;
-  return gql(registrationQuery, { login, password });
-}
+// export const actionCreateUser = (login, password) =>
+//   actionPromise('register', gqlCreateUser(login, password));
 
-const gqlGetOwnerOrders = () => {
-  const ordersQuery = `query orders($q: String) {
-    OrderFind(query: $q) {
-      _id, total, createdAt, owner{
-        _id, login
-      }, orderGoods{
-        price, count, good{
-          name
-        }
-      }
-    }
-  }`;
-  return gql(ordersQuery, { q: `[{}]` });
-}
+// export const actionOwnerOrders = () =>
+//   actionPromise('history', gqlGetOwnerOrders());
 
-const gqlCreateOrder = (orderGoods) => {
-  const orderQuery = `mutation ordering($orderGoods: OrderInput) {
-    OrderUpsert(order: $orderGoods) {
-      _id, total, orderGoods {
-        good {
-          name
-        }
-      }
-    }
-  }`;
-  return gql(orderQuery, { orderGoods: { orderGoods } });
-}
+// export const actionCreateOrder = (orderGoods) =>
+//   actionPromise('cart', gqlCreateOrder(orderGoods));
 
-export const actionCategoryById = (id) =>
-  actionPromise('category', gqlGetCategory(id));
+// export const actionFullLogin = (login, password) =>
+//   async dispatch => {
+//     //dispatch возвращает то, что вернул thunk, возвращаемый actionLogin, а там промис, 
+//     //так как actionPromise возвращает асинхронную функцию
+//     const token = await dispatch(actionLogin(login, password))
+//     //проверьте что token - строка и отдайте его в actionAuthLogin
+//     console.log(token);
+//     if (typeof token === 'string') {
+//       dispatch(actionAuthLogin(token));
+//     }
+//   }
 
-export const actionCategories = () =>
-  actionPromise('categories', gqlGetCategories());
+// export const actionFullRegister = (login, password) =>
+//   async dispatch => {
+//     await dispatch(actionCreateUser(login, password));
+//     dispatch(actionFullLogin(login, password));
+//   }
 
-export  const actionGoodById = (id) =>
-  actionPromise('good', gqlGetGood(id));
-
-export const actionLogin = (login, password) =>
-  actionPromise('login', gqlLogin(login, password));
-
-export const actionCreateUser = (login, password) =>
-  actionPromise('register', gqlCreateUser(login, password));
-
-export const actionOwnerOrders = () =>
-  actionPromise('history', gqlGetOwnerOrders());
-
-export const actionCreateOrder = (orderGoods) =>
-  actionPromise('cart', gqlCreateOrder(orderGoods));
-
-export const actionFullLogin = (login, password) =>
-  async dispatch => {
-    //dispatch возвращает то, что вернул thunk, возвращаемый actionLogin, а там промис, 
-    //так как actionPromise возвращает асинхронную функцию
-    const token = await dispatch(actionLogin(login, password))
-    //проверьте что token - строка и отдайте его в actionAuthLogin
-    console.log(token);
-    if (typeof token === 'string') {
-      dispatch(actionAuthLogin(token));
-    }
-  }
-
-export const actionFullRegister = (login, password) =>
-  async dispatch => {
-    await dispatch(actionCreateUser(login, password));
-    dispatch(actionFullLogin(login, password));
-  }
-
-export const actionOrder = (orderGoods) =>
-  async dispatch => {
-    await dispatch(actionCreateOrder(orderGoods));
-    console.log('order was created');
-    dispatch(actionCartClear());
-  }
+// export const actionOrder = (orderGoods) =>
+//   async dispatch => {
+//     await dispatch(actionCreateOrder(orderGoods));
+//     console.log('order was created');
+//     dispatch(actionCartClear());
+//   }

+ 18 - 10
js21 react/my-react-app/src/redux/reducers/index.js

@@ -4,18 +4,26 @@ import { authReducer } from "./authReducer";
 import { cartReducer } from "./cartReducer";
 import { promiseReducer } from "./promiseReducer";
 import { actionCategories, actionFullLogin} from '../../redux/actions/actions';
+import { configureStore } from '@reduxjs/toolkit';
+import categoriesReducer from '../slices/categoriesSlice';
 
+export const store = configureStore({
+  reducer: {
+    categories: categoriesReducer,
+  },
+});
 
-const reducers = {
-  promise: promiseReducer, //допилить много имен для многих промисо
-  auth: authReducer,     //часть предыдущего ДЗ
-  cart: cartReducer,     //часть предыдущего ДЗ
-}
+// const reducers = {
+//   promise: promiseReducer, //допилить много имен для многих промисо
+//   auth: authReducer,     //часть предыдущего ДЗ
+//   cart: cartReducer,     //часть предыдущего ДЗ
+// }
 
-const totalReducer = combineReducers(reducers);
+// const totalReducer = combineReducers(reducers);
+
+// const store = createStore(totalReducer, applyMiddleware(thunk));
+// store.subscribe(() => console.log(store.getState()));
+// store.dispatch(actionCategories());
+// // store.dispatch(actionFullLogin('test457', '123123'));
 
-const store = createStore(totalReducer, applyMiddleware(thunk));
-store.subscribe(() => console.log(store.getState()));
-store.dispatch(actionCategories());
-// store.dispatch(actionFullLogin('test457', '123123'));
 export default store;

+ 55 - 0
js21 react/my-react-app/src/redux/slices/categoriesSlice.js

@@ -0,0 +1,55 @@
+import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
+import { gqlGetCategories, gqlGetCategory } from '../../api/gql';
+
+const initialState = {
+  categories: [],
+  selectedCategory: [],
+  status: 'idle',
+  error: null,
+};
+
+export const fetchCategories = createAsyncThunk('catogories/fetchCategories', async () => {
+  const response = await gqlGetCategories();
+  return response;
+})
+
+export const fetchCategoryById = createAsyncThunk('categories/fetchCategoryById', async (id) => {
+  const response = await gqlGetCategory(id);
+  return response;
+})
+
+const categoriesSlice = createSlice({
+  name: 'categories',
+  initialState,
+  reducers: {},
+  extraReducers(builder) {
+    builder
+      .addCase(fetchCategories.pending, (state, action) => {
+        state.status = 'loading';
+      })
+      .addCase(fetchCategories.fulfilled, (state, action) => {
+        state.status = 'succeeded';
+        state.categories = action.payload;
+        // action?.payload;
+      })
+      .addCase(fetchCategories.rejected, (state, action) => {
+        state.status = 'failed';
+        state.error = action.error.message;
+      })
+      .addCase(fetchCategoryById.pending, (state, action) => {
+        state.status = 'loading';
+      })
+      .addCase(fetchCategoryById.fulfilled, (state, action) => {
+        state.status = 'succeeded';
+        state.selectedCategory = action.payload;
+      })
+      .addCase(fetchCategoryById.rejected, (state, action) => {
+        state.status = 'failed';
+        state.error = action.error.message;
+      })
+  },
+})
+export default categoriesSlice.reducer;
+export const selectCategories = (state) => state.categories.categories;
+export const selectSelectedCategory = (state) => state.categories.selectedCategory;
+export const selectStatus = (state) => state.categories.status;