Browse Source

login and register

viktoriia.kapran 1 năm trước cách đây
mục cha
commit
f8fc43f25c
36 tập tin đã thay đổi với 912 bổ sung412 xóa
  1. 198 0
      js21 react/my-react-app/package-lock.json
  2. 4 0
      js21 react/my-react-app/package.json
  3. 14 5
      js21 react/my-react-app/src/App.js
  4. 239 0
      js21 react/my-react-app/src/api/api.js
  5. 0 107
      js21 react/my-react-app/src/api/gql.js
  6. 50 0
      js21 react/my-react-app/src/components/CartGood.js
  7. 20 15
      js21 react/my-react-app/src/components/Categories/CategoryMenu.js
  8. 0 12
      js21 react/my-react-app/src/components/Categories/CategoryMenu.scss
  9. 15 4
      js21 react/my-react-app/src/components/Counter/Counter.js
  10. 16 0
      js21 react/my-react-app/src/components/DrawUserName.js
  11. 36 0
      js21 react/my-react-app/src/components/GoodCard/GoodCard.js
  12. 1 1
      js21 react/my-react-app/src/components/GoodCart/GoodCart.scss
  13. 0 26
      js21 react/my-react-app/src/components/GoodCart/GoodCart.js
  14. 21 14
      js21 react/my-react-app/src/components/Header.js
  15. 0 3
      js21 react/my-react-app/src/components/Image/Image.js
  16. 0 4
      js21 react/my-react-app/src/components/Image/Image.scss
  17. 0 20
      js21 react/my-react-app/src/components/ImageSlider.js
  18. 3 2
      js21 react/my-react-app/src/components/Layout.js
  19. 11 0
      js21 react/my-react-app/src/components/LinkButton.js
  20. 50 0
      js21 react/my-react-app/src/components/LoginForm.js
  21. 1 1
      js21 react/my-react-app/src/components/Price.js
  22. 1 1
      js21 react/my-react-app/src/components/Title.js
  23. 22 0
      js21 react/my-react-app/src/components/hoc/RequireAdmin.js
  24. 15 0
      js21 react/my-react-app/src/components/hoc/RequireAuth.js
  25. 1 0
      js21 react/my-react-app/src/pages/Admin.js
  26. 29 2
      js21 react/my-react-app/src/pages/Cart.js
  27. 16 20
      js21 react/my-react-app/src/pages/Category/Category.js
  28. 52 18
      js21 react/my-react-app/src/pages/Good/Good.js
  29. 2 6
      js21 react/my-react-app/src/pages/Good/Good.scss
  30. 31 3
      js21 react/my-react-app/src/pages/Login.js
  31. 6 1
      js21 react/my-react-app/src/pages/OrdersHistory.js
  32. 27 2
      js21 react/my-react-app/src/pages/Register.js
  33. 31 28
      js21 react/my-react-app/src/redux/reducers/index.js
  34. 0 24
      js21 react/my-react-app/src/redux/reducers/promiseReducer.js
  35. 0 55
      js21 react/my-react-app/src/redux/slices/categoriesSlice.js
  36. 0 38
      js21 react/my-react-app/src/redux/slices/goodSlice.js

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

@@ -13,9 +13,12 @@
         "@mui/icons-material": "^5.11.11",
         "@mui/material": "^5.11.11",
         "@reduxjs/toolkit": "^1.9.3",
+        "@rtk-query/graphql-request-base-query": "^2.0.0",
         "@testing-library/jest-dom": "^5.16.5",
         "@testing-library/react": "^13.4.0",
         "@testing-library/user-event": "^13.5.0",
+        "graphql": "^15.5.0",
+        "graphql-request": "^3.4.0",
         "react": "^18.2.0",
         "react-dom": "^18.2.0",
         "react-material-ui-carousel": "^3.4.2",
@@ -23,6 +26,7 @@
         "react-router-dom": "^6.8.2",
         "react-scripts": "5.0.1",
         "redux": "^4.2.1",
+        "redux-persist": "^6.0.0",
         "redux-thunk": "^2.4.2",
         "sass": "^1.58.3",
         "web-vitals": "^2.1.4"
@@ -3616,6 +3620,31 @@
       "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz",
       "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw=="
     },
+    "node_modules/@rtk-query/graphql-request-base-query": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/@rtk-query/graphql-request-base-query/-/graphql-request-base-query-2.2.0.tgz",
+      "integrity": "sha512-zBGaTJUHsE1UJWkQGxPfdIteysbd+4Ivx4nkuy6Xgd2kn6zOd849YdR48B1fUh7QA2CXj/F1d+jrCETULp7hnA==",
+      "dependencies": {
+        "graphql-request": "^4.0.0"
+      },
+      "peerDependencies": {
+        "@reduxjs/toolkit": "^1.7.1",
+        "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0"
+      }
+    },
+    "node_modules/@rtk-query/graphql-request-base-query/node_modules/graphql-request": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/graphql-request/-/graphql-request-4.3.0.tgz",
+      "integrity": "sha512-2v6hQViJvSsifK606AliqiNiijb1uwWp6Re7o0RTyH+uRTv/u7Uqm2g4Fjq/LgZIzARB38RZEvVBFOQOVdlBow==",
+      "dependencies": {
+        "cross-fetch": "^3.1.5",
+        "extract-files": "^9.0.0",
+        "form-data": "^3.0.0"
+      },
+      "peerDependencies": {
+        "graphql": "14 - 16"
+      }
+    },
     "node_modules/@rushstack/eslint-patch": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz",
@@ -6474,6 +6503,14 @@
         "node": ">=10"
       }
     },
+    "node_modules/cross-fetch": {
+      "version": "3.1.5",
+      "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz",
+      "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==",
+      "dependencies": {
+        "node-fetch": "2.6.7"
+      }
+    },
     "node_modules/cross-spawn": {
       "version": "7.0.3",
       "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@@ -8378,6 +8415,17 @@
       "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
       "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
     },
+    "node_modules/extract-files": {
+      "version": "9.0.0",
+      "resolved": "https://registry.npmjs.org/extract-files/-/extract-files-9.0.0.tgz",
+      "integrity": "sha512-CvdFfHkC95B4bBBk36hcEmvdR2awOdhhVUYH6S/zrVj3477zven/fJMYg7121h4T1xHZC+tetUpubpAhxwI7hQ==",
+      "engines": {
+        "node": "^10.17.0 || ^12.0.0 || >= 13.7.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jaydenseric"
+      }
+    },
     "node_modules/fast-deep-equal": {
       "version": "3.1.3",
       "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -9137,6 +9185,27 @@
       "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz",
       "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ=="
     },
+    "node_modules/graphql": {
+      "version": "15.8.0",
+      "resolved": "https://registry.npmjs.org/graphql/-/graphql-15.8.0.tgz",
+      "integrity": "sha512-5gghUc24tP9HRznNpV2+FIoq3xKkj5dTQqf4v0CpdPbFVwFkWoxOM+o+2OC9ZSvjEMTjfmG9QT+gcvggTwW1zw==",
+      "engines": {
+        "node": ">= 10.x"
+      }
+    },
+    "node_modules/graphql-request": {
+      "version": "3.7.0",
+      "resolved": "https://registry.npmjs.org/graphql-request/-/graphql-request-3.7.0.tgz",
+      "integrity": "sha512-dw5PxHCgBneN2DDNqpWu8QkbbJ07oOziy8z+bK/TAXufsOLaETuVO4GkXrbs0WjhdKhBMN3BkpN/RIvUHkmNUQ==",
+      "dependencies": {
+        "cross-fetch": "^3.0.6",
+        "extract-files": "^9.0.0",
+        "form-data": "^3.0.0"
+      },
+      "peerDependencies": {
+        "graphql": "14 - 16"
+      }
+    },
     "node_modules/gzip-size": {
       "version": "6.0.0",
       "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz",
@@ -12717,6 +12786,44 @@
         "tslib": "^2.0.3"
       }
     },
+    "node_modules/node-fetch": {
+      "version": "2.6.7",
+      "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
+      "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
+      "dependencies": {
+        "whatwg-url": "^5.0.0"
+      },
+      "engines": {
+        "node": "4.x || >=6.0.0"
+      },
+      "peerDependencies": {
+        "encoding": "^0.1.0"
+      },
+      "peerDependenciesMeta": {
+        "encoding": {
+          "optional": true
+        }
+      }
+    },
+    "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=="
+    },
+    "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=="
+    },
+    "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==",
+      "dependencies": {
+        "tr46": "~0.0.3",
+        "webidl-conversions": "^3.0.0"
+      }
+    },
     "node_modules/node-forge": {
       "version": "1.3.1",
       "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
@@ -15154,6 +15261,14 @@
         "@babel/runtime": "^7.9.2"
       }
     },
+    "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==",
+      "peerDependencies": {
+        "redux": ">4.0.0"
+      }
+    },
     "node_modules/redux-thunk": {
       "version": "2.4.2",
       "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz",
@@ -20320,6 +20435,26 @@
         }
       }
     },
+    "@rtk-query/graphql-request-base-query": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/@rtk-query/graphql-request-base-query/-/graphql-request-base-query-2.2.0.tgz",
+      "integrity": "sha512-zBGaTJUHsE1UJWkQGxPfdIteysbd+4Ivx4nkuy6Xgd2kn6zOd849YdR48B1fUh7QA2CXj/F1d+jrCETULp7hnA==",
+      "requires": {
+        "graphql-request": "^4.0.0"
+      },
+      "dependencies": {
+        "graphql-request": {
+          "version": "4.3.0",
+          "resolved": "https://registry.npmjs.org/graphql-request/-/graphql-request-4.3.0.tgz",
+          "integrity": "sha512-2v6hQViJvSsifK606AliqiNiijb1uwWp6Re7o0RTyH+uRTv/u7Uqm2g4Fjq/LgZIzARB38RZEvVBFOQOVdlBow==",
+          "requires": {
+            "cross-fetch": "^3.1.5",
+            "extract-files": "^9.0.0",
+            "form-data": "^3.0.0"
+          }
+        }
+      }
+    },
     "@rushstack/eslint-patch": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz",
@@ -22484,6 +22619,14 @@
         "yaml": "^1.10.0"
       }
     },
+    "cross-fetch": {
+      "version": "3.1.5",
+      "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz",
+      "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==",
+      "requires": {
+        "node-fetch": "2.6.7"
+      }
+    },
     "cross-spawn": {
       "version": "7.0.3",
       "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@@ -23865,6 +24008,11 @@
         }
       }
     },
+    "extract-files": {
+      "version": "9.0.0",
+      "resolved": "https://registry.npmjs.org/extract-files/-/extract-files-9.0.0.tgz",
+      "integrity": "sha512-CvdFfHkC95B4bBBk36hcEmvdR2awOdhhVUYH6S/zrVj3477zven/fJMYg7121h4T1xHZC+tetUpubpAhxwI7hQ=="
+    },
     "fast-deep-equal": {
       "version": "3.1.3",
       "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -24417,6 +24565,21 @@
       "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz",
       "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ=="
     },
+    "graphql": {
+      "version": "15.8.0",
+      "resolved": "https://registry.npmjs.org/graphql/-/graphql-15.8.0.tgz",
+      "integrity": "sha512-5gghUc24tP9HRznNpV2+FIoq3xKkj5dTQqf4v0CpdPbFVwFkWoxOM+o+2OC9ZSvjEMTjfmG9QT+gcvggTwW1zw=="
+    },
+    "graphql-request": {
+      "version": "3.7.0",
+      "resolved": "https://registry.npmjs.org/graphql-request/-/graphql-request-3.7.0.tgz",
+      "integrity": "sha512-dw5PxHCgBneN2DDNqpWu8QkbbJ07oOziy8z+bK/TAXufsOLaETuVO4GkXrbs0WjhdKhBMN3BkpN/RIvUHkmNUQ==",
+      "requires": {
+        "cross-fetch": "^3.0.6",
+        "extract-files": "^9.0.0",
+        "form-data": "^3.0.0"
+      }
+    },
     "gzip-size": {
       "version": "6.0.0",
       "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz",
@@ -27011,6 +27174,35 @@
         "tslib": "^2.0.3"
       }
     },
+    "node-fetch": {
+      "version": "2.6.7",
+      "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
+      "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
+      "requires": {
+        "whatwg-url": "^5.0.0"
+      },
+      "dependencies": {
+        "tr46": {
+          "version": "0.0.3",
+          "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+          "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=="
+        },
+        "whatwg-url": {
+          "version": "5.0.0",
+          "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+          "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+          "requires": {
+            "tr46": "~0.0.3",
+            "webidl-conversions": "^3.0.0"
+          }
+        }
+      }
+    },
     "node-forge": {
       "version": "1.3.1",
       "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
@@ -28571,6 +28763,12 @@
         "@babel/runtime": "^7.9.2"
       }
     },
+    "redux-persist": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/redux-persist/-/redux-persist-6.0.0.tgz",
+      "integrity": "sha512-71LLMbUq2r02ng2We9S215LtPu3fY0KgaGE0k8WRgl6RkqxtGfl7HUozz1Dftwsb0D/5mZ8dwAaPbtnzfvbEwQ==",
+      "requires": {}
+    },
     "redux-thunk": {
       "version": "2.4.2",
       "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz",

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

@@ -8,9 +8,12 @@
     "@mui/icons-material": "^5.11.11",
     "@mui/material": "^5.11.11",
     "@reduxjs/toolkit": "^1.9.3",
+    "@rtk-query/graphql-request-base-query": "^2.0.0",
     "@testing-library/jest-dom": "^5.16.5",
     "@testing-library/react": "^13.4.0",
     "@testing-library/user-event": "^13.5.0",
+    "graphql": "^15.5.0",
+    "graphql-request": "^3.4.0",
     "react": "^18.2.0",
     "react-dom": "^18.2.0",
     "react-material-ui-carousel": "^3.4.2",
@@ -18,6 +21,7 @@
     "react-router-dom": "^6.8.2",
     "react-scripts": "5.0.1",
     "redux": "^4.2.1",
+    "redux-persist": "^6.0.0",
     "redux-thunk": "^2.4.2",
     "sass": "^1.58.3",
     "web-vitals": "^2.1.4"

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

@@ -1,7 +1,6 @@
 import React from 'react';
 import { Provider } from 'react-redux';
-import { Route, Routes} from 'react-router-dom';
-
+import { Route, Routes } from 'react-router-dom';
 import MainPage from './pages/HomePage';
 import NotFound from './pages/NotFound';
 import OrdersHistory from './pages/OrdersHistory';
@@ -14,18 +13,28 @@ import Good from './pages/Good/Good';
 import Layout from './components/Layout';
 import store from './redux/reducers';
 import { Box } from '@mui/material';
+import RequireAuth from './components/hoc/RequireAuth';
+import RequireAdmin from './components/hoc/RequireAdmin';
 
 
 function App() {
   return (
-    <Box sx={{ height: '100vh', display: 'flex', flexDirection: 'column'}}>
+    <Box sx={{ height: '100vh', display: 'flex', flexDirection: 'column' }}>
       <Provider store={store}>
         <Routes>
           <Route path='/' element={<Layout />}>
             <Route index element={<MainPage />} />
-            <Route path='/history' element={<OrdersHistory />} />
+            <Route path='/history' element={
+              <RequireAuth>
+                <OrdersHistory />
+              </RequireAuth>
+            } />
             <Route path='/cart' element={<Cart />} />
-            <Route path='/admin' element={<Admin />} />
+            <Route path='/admin' element={
+              <RequireAdmin>
+                <Admin />
+              </RequireAdmin>
+            } />
             <Route path='/register' element={<Register />} />
             <Route path='/login' element={<Login />} />
             <Route path='/:category/:categoryId' element={<Category />} />

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

@@ -0,0 +1,239 @@
+import { createApi } from '@reduxjs/toolkit/query/react'
+import { createSlice } from "@reduxjs/toolkit";
+import { gql } from 'graphql-request'
+import { graphqlRequestBaseQuery } from '@rtk-query/graphql-request-base-query'; //npm install
+
+
+const API_URL = "http://shop-roles.node.ed.asmer.org.ua/graphql";
+
+const prepareHeaders = (headers, { getState }) => {
+  // By default, if we have a token in the store, let's use that for authenticated requests
+  const token = getState().auth?.token || null;
+  if (token) {
+    headers.set("Authorization", `Bearer ${token}`);
+  }
+  return headers;
+}
+
+export const api = createApi({
+  reducerPath: 'api',
+  baseQuery: graphqlRequestBaseQuery({
+    url: API_URL,
+    prepareHeaders
+  }),
+  endpoints: (builder) => ({
+    getCategories: builder.query({
+      query: () => ({
+        document: gql`
+                  query GetCategories{
+                      CategoryFind(query: "[{\\"parent\\": null}]") {
+                        _id name
+                      }
+                    }
+                  `}),
+    }),
+    getCategoryById: builder.query({
+      query: (_id) => ({
+        document: gql`
+                  query GetCategory($q: String) {
+                      CategoryFindOne(query: $q) {
+                        _id
+                        name,
+                        goods{
+                          name,
+                          _id,
+                          images{
+                            _id,
+                            url
+                          },
+                          price
+                        },
+                        parent {
+                          _id,
+                          name
+                        },
+                        subCategories{
+                          name,
+                          _id
+                          subCategories{
+                            name,
+                            _id
+                          }
+                        }
+                      }
+                  }
+                  `,
+        variables: { q: JSON.stringify([{ _id }]) }
+      }),
+    }),
+    getGoodById: builder.query({
+      query: (_id) => ({
+        document: gql`
+                  query GetGood($q: String) {
+                    GoodFindOne(query: $q) {
+                      _id,
+                      name,
+                      categories{
+                        _id,
+                        name
+                      },
+                      description,
+                      price,
+                      images{
+                        _id,
+                        url
+                      }
+                    }
+                   }
+                  `,
+        variables: { q: JSON.stringify([{ _id }]) }
+      }),
+    }),
+    login: builder.mutation({
+      query: ({ login, password }) => ({
+        document: gql`
+                  query login($login: String, $password: String) {
+                      login(login: $login, password: $password) 
+                  }
+                  `,
+        variables: { login, password }
+      })
+    }),
+    register: builder.mutation({
+      query: ({ login, password }) => ({
+        document: gql`
+                  mutation registration($login:String, $password: String){
+                      UserUpsert(user: {login:$login, password: $password}){
+                           _id login createdAt
+                        }
+                  }`,
+        variables: { login, password }
+      })
+    }),
+    getOwnerOrder: builder.query({
+      query: () => ({
+        document: gql`
+                  query orders($q: String) {
+                    OrderFind(query: $q) {
+                      _id, total, createdAt, owner{
+                        _id, login
+                      }, orderGoods{
+                        price, count, good{
+                          name
+                        }
+                      }
+                    }
+                  }
+                  `,
+        variables: { q: JSON.stringify([{}]) }
+      })
+    })
+  }),
+});
+
+function jwtDecode(token) {
+  try {
+    const tokenArr = token.split(".");
+    const tokenJsonStr = atob(tokenArr[1]);
+    const tokenJson = JSON.parse(tokenJsonStr);
+    return tokenJson;
+  }
+  catch (error) { }
+}
+export const authSlice = createSlice({
+  name: 'auth',
+  initialState: {},
+  reducers: {
+    login(state, { payload }) {
+      const tokenPayload = jwtDecode(payload);
+      if (tokenPayload) {
+        state.token = payload;
+        state.payload = tokenPayload;
+        state.error = false;
+      }
+    },
+    logout() {
+      return {};
+    }
+  },
+  extraReducers: (builder) => {
+    builder.addMatcher(
+      api.endpoints.login.matchFulfilled,
+      (state, { payload }) => {
+        if (!payload.login) {
+          state.error = true;
+        }
+      }
+    )
+  },
+})
+
+export const actionFullLogin = ({ login, password }) =>
+  async (dispatch) => {
+    const payload = await dispatch(api.endpoints.login.initiate({ login, password }))
+    if (payload.data.login)
+      dispatch(authSlice.actions.login(payload.data.login));
+  }
+
+export const actionFullRegister = (login, password) =>
+  async dispatch => {
+    const payload = await dispatch(api.endpoints.register.initiate(login, password));
+    console.log(payload);
+    dispatch(actionFullLogin(login, password));
+  }
+
+const getItemIndex = (state, idToFind) => {
+  const idsArr = state.goods.map(item => item.good._id);
+  return idsArr.indexOf(idToFind);
+}
+const initialState = {
+  goods: [],
+  totalAmount: 0,
+  goodsCount: 0
+}
+
+export const cartSlice = createSlice({
+  name: 'cart',
+  initialState,
+  reducers: {
+    addGood(state, action) {
+      const itemIndex = getItemIndex(state, action.payload.good._id);
+      if (itemIndex && itemIndex < 0)
+        state.goods.push(action.payload);
+      else {
+        state.goods[itemIndex].count += +action.payload.count;
+      }
+      state.totalAmount += action.payload.count * action.payload.good.price;
+      state.goodsCount += action.payload.count;
+    },
+    decreaseGoodCount(state, action) {
+      const itemIndex = getItemIndex(state, action.payload.good._id);
+      if (state.goods[itemIndex].count > 1) {
+        state.goods[itemIndex].count -= 1;
+      }
+      else {
+        state.goods = state.goods.filter(item => item.good._id !== action.payload.good._id);
+      }
+      state.goodsCount -= 1;
+      state.totalAmount -= action.payload.good.price;
+    },
+    setGoodCount(state, action) {
+      const itemIndex = getItemIndex(state, action.payload.good._id);
+      state.goods[itemIndex].count = action.payload?.count;
+      //state.totalAmount += action.payload.count * action.payload.good.price;
+      //задать для totalAmount и goodsCount
+    },
+    deleteGood(state, action) {
+      state.goods = state.goods.filter(item => item.good._id !== action.payload.good._id);
+      state.totalAmount -= action.payload.count * action.payload.good.price;
+      state.goodsCount -= action.payload.count;
+    },
+    clearCart() {
+      return initialState;
+    }
+
+
+  },
+});
+export const { addGood, deleteGood, decreaseGoodCount, setGoodCount, clearCart } = cartSlice.actions;
+export const { useGetCategoriesQuery, useGetCategoryByIdQuery, useGetGoodByIdQuery, useLoginMutation, useGetOwnerOrderQuery, useRegisterMutation } = api;

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

@@ -1,110 +1,3 @@
-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}"}]` });
-}
-
-export 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)

+ 50 - 0
js21 react/my-react-app/src/components/CartGood.js

@@ -0,0 +1,50 @@
+import React, { useEffect, useState } from 'react';
+import Card from '@mui/material/Card';
+import CardContent from '@mui/material/CardContent';
+import Box from '@mui/material/Box';
+import AddIcon from '@mui/icons-material/Add';
+import RemoveIcon from '@mui/icons-material/Remove';
+import DeleteOutlineOutlinedIcon from '@mui/icons-material/DeleteOutlineOutlined';
+import IconButton from '@mui/material/IconButton';
+import { Price } from './Price';
+import { Counter } from './Counter/Counter';
+import { addGood, deleteGood, decreaseGoodCount, setGoodCount, clearCart } from '../api/api';
+import { useDispatch, useSelector } from 'react-redux';
+import { Image } from './Image/Image';
+import { Stack } from '@mui/material';
+
+
+export default function CartGood({ good }) {
+  const [totalSum, setTotalSum] = useState(good.good.price * good.count);
+  const dispatch = useDispatch();
+  const count = useSelector(state => state.cart.goods.find(findedGood => findedGood.good._id === good.good._id)?.count);
+  useEffect(() => {
+    setTotalSum(good.good.price * count);
+  }, [count]);
+  const decrease = (good) => {
+    dispatch(decreaseGoodCount(good));
+  }
+  return (
+    <Card key={good.good._id} sx={{ mb: 2 }}>
+      <CardContent>
+        <Stack direction="row" alignItems="center" spacing={2}>
+          <Box sx={{ height: '100px', width: '100px' }}>
+            <Image url={good?.good?.images[0]?.url} />
+          </Box>
+          <Box sx={{ flexGrow: 1 }}>{good.good.name}</Box>
+          <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'end' }}>
+            <RemoveIcon sx={{ cursor: 'pointer' }} onClick={() => decrease({ good: good.good })} />
+            <Counter value={count} onCount={(countValue) => dispatch(setGoodCount({good: good.good, count: countValue}))} />
+            <AddIcon sx={{ cursor: 'pointer' }} onClick={() => dispatch(addGood({ good: good.good, count: 1 }))} />
+          </Box>
+          <Box>
+            <Price>{totalSum}</Price>
+          </Box>
+          <IconButton onClick={() => dispatch(deleteGood({ good: good.good, count }))}>
+            <DeleteOutlineOutlinedIcon />
+          </IconButton>
+        </Stack>
+      </CardContent>
+    </Card>
+  )
+}

+ 20 - 15
js21 react/my-react-app/src/components/Categories/CategoryMenu.js

@@ -1,23 +1,28 @@
-import React, { useEffect } from 'react';
+import React from 'react';
 import { Link } from 'react-router-dom';
-import { useDispatch, useSelector } from 'react-redux';
 import './CategoryMenu.scss'
-import { fetchCategories, selectCategories } from '../../redux/slices/categoriesSlice';
 import Skeleton from '@mui/material/Skeleton';
-import { Box } from '@mui/material';
-
+import List from '@mui/material/List';
+import ListItem from '@mui/material/ListItem';
+import ListItemButton from '@mui/material/ListItemButton';
+import ListItemText from '@mui/material/ListItemText';
+import { useGetCategoriesQuery } from '../../api/api';
 
 export const CategoryMenu = () => {
-  const categories = useSelector(selectCategories);
-  const dispatch = useDispatch();
-  useEffect(() => {
-    dispatch(fetchCategories());
-  }, [dispatch]);
-  console.log('categories', categories)
+  const { data, error, isFetching } = useGetCategoriesQuery();
+
   return (
-    <Box className='aside' component='aside'>
-      {categories?.length == 0 && Array(10).fill(1).map((item, index) => <Skeleton key={index} className='skeleton' />)}
-      {categories?.map(category => <Link key={category._id} to={`/${category.name}/${category._id}`}>{category.name}</Link>)}
-    </Box>
+    <aside className='aside'>
+      {isFetching ? Array(10).fill(1).map((_, index) => <Skeleton key={index} className='skeleton' />) :
+        <List>
+          {data.CategoryFind.map(category =>
+            <ListItem key={category._id} disablePadding>
+              <ListItemButton component={Link} to={`/${category.name}/${category._id}`}>
+                <ListItemText primary={category.name} sx={{ color: '#000' }} />
+              </ListItemButton>
+            </ListItem>
+          )}
+        </List>}
+    </aside>
   )
 }

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

@@ -2,18 +2,6 @@
   min-width: 260px;
   max-width: 260px;
   margin-right: 30px;
-  a {
-    display: block;
-    text-decoration: none;
-    color: black;
-    border-bottom: 1px solid gray;
-    padding: 10px 20px;
-    margin-bottom: 8px;
-  }
-  a:hover {
-    color: gray;
-    transition: color 0.3s;
-  }
   .skeleton {
     padding: 10px 20px;
     margin-bottom: 8px;

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

@@ -1,16 +1,27 @@
-import React from "react";
+import React, { useState, useEffect } from "react";
 import TextField from '@mui/material/TextField';
 import Box from '@mui/material/Box';
 import './Counter.scss'
 
-export function Counter({ value }) {
+export function Counter({ value, onCount }) {
+  const [countValue, setCountValue] = useState(value);
+  useEffect(() => setCountValue(value), [value]);
+
   return (
-    <Box sx={{ width: '50px'}}>
+    <Box sx={{ width: '50px', mx: '6px'}}>
       <TextField
         type="number"
         min="1"
-        value={value}
+        value={countValue}
         size="small"
+        onChange={e => {
+          console.log(e.target.value);
+          setCountValue(e.target.value);
+          if (e.target.value !== '') { // emit only valid values
+            onCount(+e.target.value);
+          } 
+        }
+      }
       />
     </Box>
   )

+ 16 - 0
js21 react/my-react-app/src/components/DrawUserName.js

@@ -0,0 +1,16 @@
+import React from "react";
+import { useSelector } from 'react-redux';
+import Box from '@mui/material/Box';
+import store from '../redux/reducers';
+
+const DrawUserName = () => {
+  const token = useSelector(state => state.auth.token);
+
+  return (
+    <>
+      {token ? <Box>Hello, {store.getState().auth?.payload?.sub?.login}</Box> : <></>}
+    </>
+
+  )
+}
+export default DrawUserName;

+ 36 - 0
js21 react/my-react-app/src/components/GoodCard/GoodCard.js

@@ -0,0 +1,36 @@
+import React, { useState } from 'react';
+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 './GoodCard.scss'
+import { Image } from '../Image/Image';
+import { Price } from '../Price';
+import { Counter } from '../Counter/Counter.js';
+import { Box, Button } from '@mui/material';
+import { useDispatch } from 'react-redux';
+import { addGood } from '../../api/api';
+
+
+
+export default function GoodCard({ good }) {
+  const [count, setCount] = useState(1);
+  const dispatch = useDispatch();
+  return (
+    <Card sx={{ width: 275 }}>
+      <CardContent className='good-card'>
+        <Link to={`/good/${good?._id}`} className='good-name'>{good?.name}</Link>
+        {good?.images?.length && <Link to={`/good/${good?._id}`}>
+          <Box sx={{ width: '200px', height: '200px', m: '0 auto'}}>
+          <Image url={`${good?.images[0]?.url}`} />
+          </Box>
+          </Link>}
+        <Price>{good?.price}</Price>
+        <Counter value={count} onCount={(countValue) => setCount(countValue)} />
+        <CardActions>
+          <Button variant="contained" onClick={() => {dispatch(addGood({ good, count: +count }))}}>Add to cart</Button>
+        </CardActions>
+      </CardContent>
+    </Card>
+  )
+}

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

@@ -1,4 +1,4 @@
-.good-cart {
+.good-card {
   display: flex;
   flex-direction: column;
   align-items: center;

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

@@ -1,26 +0,0 @@
-import React from 'react';
-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';
-import { Counter } from '../Counter/Counter.js';
-import { Button } from '@mui/material';
-
-
-
-export default function GoodCart({ good }) {
-  return (
-    <Card sx={{ width: 275 }}>
-      <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} />
-        <Button variant="contained">Add to cart</Button>
-      </CardContent>
-    </Card>
-  )
-}

+ 21 - 14
js21 react/my-react-app/src/components/Header.js

@@ -7,12 +7,11 @@ import ShoppingCartIcon from '@mui/icons-material/ShoppingCart';
 import AppBar from '@mui/material/AppBar';
 import Box from '@mui/material/Box';
 import Toolbar from '@mui/material/Toolbar';
-import Typography from '@mui/material/Typography';
 import { Button } from '@mui/material';
-
-// const UserName = ({userName}) => <div>{userName || 'anon'}</div>
-
-// const ConnectUserName = connect(state => ({userName: state.auth.payload?.sub?.login}))(UserName);
+import { useDispatch, useSelector } from 'react-redux';
+import { authSlice } from '../api/api';
+import DrawUserName from './DrawUserName';
+import LinkButton from './LinkButton';
 
 const StyledBadge = styled(Badge)(({ theme }) => ({
   '& .MuiBadge-badge': {
@@ -23,6 +22,12 @@ const StyledBadge = styled(Badge)(({ theme }) => ({
   },
 }));
 export default function Header() {
+  const dispatch = useDispatch();
+  const goodsInCart = useSelector(state => state.cart.goodsCount);
+  const onLogout = () => {
+    dispatch(authSlice.actions.logout());
+  }
+  const token = useSelector(state => state.auth.token);
   return (
     <AppBar position="static">
       <Toolbar>
@@ -30,25 +35,27 @@ export default function Header() {
           <Link to="/"><img src="/images/logo.jpg" /></Link>
         </Box>
         <Box sx={{ display: 'flex', alignItems: 'center' }}>
+          <DrawUserName />
           <Box sx={{ mx: 1, my: 2 }}> {/* отображать, когда залогинен */}
-            <Button to="history" component={Link} color="inherit">My orders</Button>
+            {token &&
+              <Button to="history" component={Link} color="inherit">My orders</Button>}
           </Box>
           <Box sx={{ mx: 1, my: 2 }}>
             <Link to="cart">
               <IconButton aria-label="cart">
-                <StyledBadge badgeContent={1} color="primary">
+                <StyledBadge badgeContent={goodsInCart} color="primary">
                   <ShoppingCartIcon sx={{ color: 'white', fontSize: '30px' }} />
                 </StyledBadge>
               </IconButton>
             </Link>
           </Box>
-          {/* <ConnectUserName /> */}
-          <Box sx={{ mx: 1, my: 2 }}>
-            <Button to="login" component={Link} color="inherit">Login</Button>
-          </Box>
-          <Box sx={{ mx: 1, my: 2 }}>
-            <Button to="register" component={Link} color="inherit">Registration</Button>
-          </Box>
+          {token ?
+            <LinkButton to="/" text="Logout" click={onLogout}/> : <>
+              <LinkButton to="login" text="Login" />
+              <LinkButton to="register" text="Registration" />
+            </>
+          }
+
           {/* <button id="logout" className="button">Logout</button> */}
         </Box>
       </Toolbar>

+ 0 - 3
js21 react/my-react-app/src/components/Image/Image.js

@@ -1,12 +1,9 @@
-import { Box } from "@mui/material";
 import React from "react";
 import './Image.scss';
 
 export function Image({url}) {
 
   return (
-    <Box className="image-container">
       <img className="good-image" src={`http://shop-roles.node.ed.asmer.org.ua/${url}`}/>
-    </Box>
   )
 }

+ 0 - 4
js21 react/my-react-app/src/components/Image/Image.scss

@@ -1,10 +1,6 @@
-.image-container {
-  width: 200px;
-  height: 200px;
 
   .good-image {
     width: 100%;
     height: 100%;
     object-fit: contain;
   }
-}

+ 0 - 20
js21 react/my-react-app/src/components/ImageSlider.js

@@ -1,20 +0,0 @@
-import { Box } from "@mui/material";
-import {React, useState} from "react";
-import { Image } from "./Image/Image";
-
-export default function ImageSlider({images}) {
-  const [selectedImage, setSelectedImage] = useState(images[0]?.url);
-  const [allImages, setAllImages] = useState(images);
-  console.log(images);
-  return (
-    <Box>
-      <Image url={selectedImage}/>
-      <Box sx={{display: 'flex'}}>
-        {allImages?.map(image => <Image key={image?._id}
-                    onClick={() => {console.log('click');setSelectedImage(`${image?.url}`)}}
-                    url={`${image?.url}`}
-                    />)}
-      </Box>
-    </Box>
-  )
-}

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

@@ -4,13 +4,14 @@ import Footer from './Footer';
 import Header from './Header';
 import { CategoryMenu, ReduxCategoryMenu } from './Categories/CategoryMenu';
 import Box from '@mui/material/Box';
+import { Container } from '@mui/material';
 
 
 export default function Layout() {
   return (
     <>
       <Header />
-      <Box sx={{ display: 'flex',
+      <Container sx={{ display: 'flex',
                 mx: 'auto',
                 position: 'relative',
                 width: '100%',
@@ -21,7 +22,7 @@ export default function Layout() {
         <Box sx={{ margin: '0 auto', width: '100%' }}>
           <Outlet />
         </Box>
-      </Box>
+      </Container>
       <Footer />
     </>
   )

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

@@ -0,0 +1,11 @@
+import { Box, Button } from '@mui/material';
+import React from 'react';
+import { Link } from 'react-router-dom';
+
+export default function LinkButton({to, text, click} ) {
+  return (
+    <Box sx={{ mx: 1, my: 2 }}>
+              <Button to={to} component={Link} color="inherit" onClick={click}>{text}</Button>
+            </Box>
+  )
+}

+ 50 - 0
js21 react/my-react-app/src/components/LoginForm.js

@@ -0,0 +1,50 @@
+import React, { useState } from 'react';
+import InputAdornment from '@mui/material/InputAdornment';
+import OutlinedInput from '@mui/material/OutlinedInput';
+import IconButton from '@mui/material/IconButton';
+import Visibility from '@mui/icons-material/Visibility';
+import VisibilityOff from '@mui/icons-material/VisibilityOff';
+import { FormControl, Button } from '@mui/material';
+
+const LoginForm = ({submit, onSubmit}) => {
+  const [login, setLogin] = useState('');
+  const [password, setPassword] = useState('');
+  const [showPassword, setShowPassword] = useState(false);
+  const handleClickShowPassword = () => setShowPassword((show) => !show);
+  const handleMouseDownPassword = (event) => {
+    event.preventDefault();
+  };
+  return (
+    <>
+      <FormControl variant="outlined">
+        <OutlinedInput
+          placeholder="login"
+          type="text"
+          value={login}
+          onChange={e => setLogin(e.target.value)} />
+      </FormControl>
+      <FormControl variant="outlined">
+        <OutlinedInput
+          placeholder="password"
+          type={showPassword ? 'text' : 'password'} value={password}
+          onChange={e => setPassword(e.target.value)}
+          endAdornment={
+            <InputAdornment position="end">
+              <IconButton
+                aria-label="toggle password visibility"
+                onClick={handleClickShowPassword}
+                onMouseDown={handleMouseDownPassword}
+                edge="end"
+              >
+                {showPassword ? <VisibilityOff /> : <Visibility />}
+              </IconButton>
+            </InputAdornment>
+          } />
+      </FormControl>
+      <Button variant="contained" onClick={() => onSubmit(login, password)}>{submit}</Button>
+      </>
+
+  )
+}
+
+export default LoginForm;

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

@@ -4,7 +4,7 @@ import React from "react";
 
 export function Price( {children}) {
   return (
-    <Box sx={{ fontSize: '18px', fontWeight: '400', color: 'rgb(15, 29, 67)'}}>
+    <Box sx={{ fontSize: '22px', fontWeight: '400', color: 'rgb(58, 78, 88)'}}>
       {children} UAH
     </Box>
   )

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

@@ -2,7 +2,7 @@ import { Typography } from "@mui/material";
 import React from "react";
 
 
-export function Title({children}) {
+export default function Title({children}) {
   return (
     <Typography variant="h4" component="h1" sx={{ textAlign: 'center', fontWeight: '300', my: '20px', mx: '0'}}>
       {children}

+ 22 - 0
js21 react/my-react-app/src/components/hoc/RequireAdmin.js

@@ -0,0 +1,22 @@
+import React from 'react';
+import { useSelector } from 'react-redux';
+import { Navigate, useLocation } from 'react-router-dom';
+
+function RequireAdmin({ children }) {
+  const location = useLocation();
+  const id = useSelector(state => state.auth.payload.sub.id);
+  const adminId = '6267f1f2bf8b206433f5b409';
+  if(!(id === adminId)) {
+    return <Navigate to={'/'} state={{from: location.pathname}}/>
+  }
+  return (
+    <>
+      {id === adminId && children}
+    </>
+
+  )
+
+}
+
+export default RequireAdmin;
+

+ 15 - 0
js21 react/my-react-app/src/components/hoc/RequireAuth.js

@@ -0,0 +1,15 @@
+import React from 'react';
+import { useSelector } from 'react-redux';
+import { Navigate, useLocation } from 'react-router-dom';
+
+function RequireAuth({children}) {
+const location = useLocation();
+const auth = useSelector(state => state.auth.token);
+if (!auth) {
+  return <Navigate to={'/login'} state={{from: location.pathname}}/>
+}
+
+  return children;
+}
+
+export default RequireAuth;

+ 1 - 0
js21 react/my-react-app/src/pages/Admin.js

@@ -5,3 +5,4 @@ export default function Admin() {
     <div>Admin</div>
   )
 }
+//eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOnsiaWQiOiI2MjY3ZjFmMmJmOGIyMDY0MzNmNWI0MDkiLCJsb2dpbiI6ImFkbWluIiwiYWNsIjpbIjYyNjdmMWYyYmY4YjIwNjQzM2Y1YjQwOSIsInVzZXIiLCJhZG1pbiJdfSwiaWF0IjoxNjc5MDkzNjQ5fQ.J1vvj_0OfnBYEFdylLZdwSoXuA29KHLYjdib0z45IOI

+ 29 - 2
js21 react/my-react-app/src/pages/Cart.js

@@ -1,7 +1,34 @@
-import React from 'react'
+import React, { useState } from 'react';
+import Title from '../components/Title';
+import store from '../redux/reducers';
+import { clearCart } from '../api/api';
+import CartGood from '../components/CartGood';
+import { useDispatch, useSelector } from 'react-redux';
+import { Box, Button } from '@mui/material';
+import { Price } from '../components/Price';
 
 export default function Cart() {
+  const goods = useSelector(state => state.cart.goods);
+  const totalAmount = useSelector(state => state.cart.totalAmount);
+  const dispatch = useDispatch();
+  console.log(goods);
   return (
-    <div>Cart</div>
+    <>
+      <Title>Cart</Title>
+      <Box sx={{ display: 'flex', justifyContent: 'flex-end', m: '4px' }}>
+        <Button
+          variant="text"
+          size='small'
+          color='inherit'
+          sx={{ maxWidth: '100px' }}
+          onClick={() => dispatch(clearCart())}
+        >
+          Clear cart
+        </Button>
+      </Box>
+      {goods?.map((good) =>
+        <CartGood good={good} key={good.good._id} />)}
+        <Price>{totalAmount}</Price>
+    </>
   )
 }

+ 16 - 20
js21 react/my-react-app/src/pages/Category/Category.js

@@ -1,34 +1,30 @@
 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';
+import Title from '../../components/Title';
 import { CategoriesSection } from '../../components/CategoriesSection/CategoriesSection';
-import GoodCart from '../../components/GoodCart/GoodCart';
+import GoodCard from '../../components/GoodCard/GoodCard';
 import './Category.scss';
 import Loader from '../../components/Loader';
 import { Box } from "@mui/material";
+import { useGetCategoryByIdQuery } from '../../api/api';
 
 const Category = () => {
   const { categoryId } = useParams();
-  const dispatch = useDispatch();
-  const status = useSelector(selectStatus);
-  useEffect(() => {
-    dispatch(fetchCategoryById(categoryId));
-  }, [dispatch, categoryId]);
-  const category = useSelector(selectSelectedCategory);
+  const { data, error, isFetching } = useGetCategoryByIdQuery(categoryId);
+
   return (
     <>
-      {status === 'loading' && <Loader />}
-      {status === 'succeeded' && <>
-        <Title>{category?.name}</Title>
-        {category?.subCategories?.length > 0 && <CategoriesSection key={category._id} categories={category?.subCategories} categoryEl='Subcategories:' />}
-        {/* {category?.parent?.length > 0 && <CategoriesSection categories={category?.parent} categoryEl='Parent Category:'/>} */}
-        {/* спросить Андрея про parent */}
-        <Box className='goods-container'>
-          {category?.goods?.map(good => <GoodCart key={good._id} good={good} />)}
-        </Box>
-      </>}
+      {isFetching ? <Loader /> :
+        <>
+            <Title>{data.CategoryFindOne?.name}</Title>
+            {data.CategoryFindOne?.subCategories?.length > 0 && <CategoriesSection key={data.CategoryFindOne?._id} categories={data.CategoryFindOne?.subCategories} categoryEl='Subcategories:' />}
+            {data.CategoryFindOne?.parent && <CategoriesSection categories={[data.CategoryFindOne?.parent]} categoryEl='Parent category:' />}
+
+            <Box className='goods-container'>
+              {data.CategoryFindOne?.goods?.map(good => <GoodCard key={good._id} good={good} />)}
+            </Box>
+        </>}
+
     </>
   )
 }

+ 52 - 18
js21 react/my-react-app/src/pages/Good/Good.js

@@ -1,32 +1,66 @@
 
-import React, { useEffect } from 'react'
-import { useDispatch, useSelector } from 'react-redux';
+import React, { useEffect, useState } from 'react';
 import { useParams } from 'react-router-dom';
+import { useDispatch } from 'react-redux';
 import { CategoriesSection } from '../../components/CategoriesSection/CategoriesSection';
 import Loader from '../../components/Loader';
-import { Title } from '../../components/Title';
-import { fetchGoodById, selectGood, selectGoodStatus } from '../../redux/slices/goodSlice';
+import Title from '../../components/Title';
+import { useGetGoodByIdQuery } from '../../api/api';
+import { Box } from '@mui/system';
+import Carousel from 'react-material-ui-carousel';
+import { Image } from '../../components/Image/Image';
+import { Price } from '../../components/Price';
+import { Counter } from '../../components/Counter/Counter';
+import { Button } from '@mui/material';
 import './Good.scss';
-import ImageSlider from '../../components/ImageSlider';
+import { addGood, deleteGood } from '../../api/api';
 
 const Good = () => {
   const { goodId } = useParams();
+  const [count, setCount] = useState(1);
+  const { data, error, isFetching } = useGetGoodByIdQuery(goodId);
   const dispatch = useDispatch();
-  const status = useSelector(selectGoodStatus);
-  useEffect(() => {
-    dispatch(fetchGoodById(goodId));
-  }, [dispatch, goodId]);
-  const good = useSelector(selectGood);
+  const add = () => {
+    dispatch(addGood({good: data.GoodFindOne, count: +count}));
+  }
+  const deleteG = () => {
+    dispatch(deleteGood({good: data.GoodFindOne}));
+  }
   return (
     <>
-      {status === 'loading' && <Loader />}
-      {status === 'succeeded' && <>
-      <Title>{good?.name}</Title>
-      {good?.categories?.length > 0 && <CategoriesSection key={good?.categories?._id} categories={good?.categories} categoryEl='Category:' />}
-      <section className='good-section'>
-        <ImageSlider images={good?.images}/>
-      </section>
-      </>}
+      {isFetching ? <Loader /> :
+        <>
+          <Title>{data.GoodFindOne?.name}</Title>
+          {data.GoodFindOne?.categories?.length > 0 && <CategoriesSection key={data.GoodFindOne?.categories?._id} categories={data.GoodFindOne?.categories} categoryEl='Category:' />}
+          <Box sx={{ display: 'flex', my: '20px', width: '100%' }}>
+            <Box sx={{ width: '50%', pr: '30px' }}>
+              <Carousel>
+                {
+                  data.GoodFindOne?.images.map((image, i) => <Image key={i} url={image?.url} />)
+                }
+              </Carousel>
+            </Box>
+            <Box sx={{ width: '50%', display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
+              <Box className='info-about-good'>
+                {data.GoodFindOne?.description}
+              </Box>
+              <Box className='info-about-good'>
+                <Price>{data.GoodFindOne?.price}</Price>
+              </Box>
+              <Box className='info-about-good'>
+                <Counter value={1} onCount={(countValue) => setCount(countValue)} />
+              </Box>
+              <Box className='info-about-good'>
+                <Button variant="contained" onClick={() => add()}>Add to cart</Button>
+              </Box>
+              <Box className='info-about-good'>
+                <Button variant="contained" onClick={() => deleteG()}>Delete from cart</Button>
+              </Box>
+
+
+            </Box>
+          </Box>
+        </>}
     </>
   )
 }

+ 2 - 6
js21 react/my-react-app/src/pages/Good/Good.scss

@@ -1,7 +1,3 @@
-.good-section {
-  display: flex;
-  margin: 20px 0;
-  * {
-    width: 50%;
-  }
+.info-about-good + .info-about-good {
+  margin-top: 16px;
 }

+ 31 - 3
js21 react/my-react-app/src/pages/Login.js

@@ -1,7 +1,35 @@
-import React from 'react'
+import { Button, FormControl, Stack } from '@mui/material';
+import React, { useEffect, useState } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import { actionFullLogin } from '../api/api';
+import LoginForm from '../components/LoginForm';
+import Alert from '@mui/material/Alert';
+import { useNavigate } from 'react-router-dom';
+import Title from '../components/Title';
+
+const Login = () => {
+  const token = useSelector(state => state.auth.token);
+  const error = useSelector(state => state.auth.error);
+  const dispatch = useDispatch();
+  const navigate = useNavigate();
+
+  useEffect(() => {
+    if (token) {
+      navigate('/');
+    }
+  }, [token]);
+  const onLogin = (login, password) => {
+    dispatch(actionFullLogin({ login, password }));
+  }
 
-export default function Login() {
   return (
-    <div>Login</div>
+    <>
+    <Title>Login</Title>
+      <Stack sx={{ maxWidth: '300px', width: '100%', m: '150px auto' }} spacing={2}>
+        <LoginForm submit='Login' onSubmit={onLogin} />
+        {error && <Alert severity="error">You entered wrong login or password!</Alert>}
+      </Stack>
+    </>
   )
 }
+export default Login;

+ 6 - 1
js21 react/my-react-app/src/pages/OrdersHistory.js

@@ -1,7 +1,12 @@
 import React from 'react'
+import { useGetOwnerOrderQuery } from '../api/api';
+import Title from '../components/Title';
 
 export default function OrdersHistory() {
+  const { data, error, isFetching } = useGetOwnerOrderQuery();
+  console.log(data?.GetOwnerOrder);
   return (
-    <div>OrdersHistory</div>
+    <Title>My orders</Title>
+    
   )
 }

+ 27 - 2
js21 react/my-react-app/src/pages/Register.js

@@ -1,7 +1,32 @@
-import React from 'react'
+import { Stack } from '@mui/material';
+import React, { useEffect } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import { actionFullRegister } from '../api/api';
+import LoginForm from '../components/LoginForm';
+import { useNavigate } from 'react-router-dom';
+import Title from '../components/Title';
 
 export default function Register() {
+  const token = useSelector(state => state.auth.token);
+  const dispatch = useDispatch();
+  const navigate = useNavigate();
+
+  useEffect(() => {
+    if (token) {
+      navigate('/');
+    }
+  }, [token]);
+
+  const onRegister = (login, password) => {
+    dispatch(actionFullRegister({ login, password }));
+  }
+
   return (
-    <div>Register</div>
+    <>
+    <Title>Registration</Title>
+      <Stack sx={{ maxWidth: '300px', width: '100%', m: '150px auto' }} spacing={2}>
+        <LoginForm submit='Registration' onSubmit={onRegister} />
+      </Stack>
+    </>
   )
 }

+ 31 - 28
js21 react/my-react-app/src/redux/reducers/index.js

@@ -1,36 +1,39 @@
-import { createStore, combineReducers, applyMiddleware } from 'redux';
-import thunk from 'redux-thunk';
-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';
-import goodReducer from '../slices/goodSlice';
-import authReducer, { login, logout } from '../slices/authSlice';
+import { api, authSlice, cartSlice } from '../../api/api';
+import storage from 'redux-persist/lib/storage';
+import { combineReducers } from 'redux';
 
-export const store = configureStore({
-  reducer: {
-    categories: categoriesReducer,
-    good: goodReducer,
-    auth: authReducer
-  },
-});
+import {
+  persistReducer, persistCombineReducers, persistStore, FLUSH,
+  REHYDRATE,
+  PAUSE,
+  PERSIST,
+  PURGE,
+  REGISTER,
+} from 'redux-persist';
+
+const persistConfig = {
+  key: 'all',
+  storage,
+  blacklist: [api.reducerPath] //стейт каких редьюсеров не хранить в localstorage
+};
 
-// const reducers = {
-//   promise: promiseReducer, //допилить много имен для многих промисо
-//   auth: authReducer,     //часть предыдущего ДЗ
-//   cart: cartReducer,     //часть предыдущего ДЗ
-// }
+const persistedReducer = persistCombineReducers(
+  persistConfig,
+  {
+    [api.reducerPath]: api.reducer,
+    [authSlice.name]: authSlice.reducer,
+    [cartSlice.name]: cartSlice.reducer,
+  });
 
-// const totalReducer = combineReducers(reducers);
 
-// const store = createStore(totalReducer, applyMiddleware(thunk));
-store.subscribe(() => console.log(store.getState()));
-const token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOnsiaWQiOiI2Mzc3ZTEzM2I3NGUxZjVmMmVjMWMxMjUiLCJsb2dpbiI6InRlc3Q1IiwiYWNsIjpbIjYzNzdlMTMzYjc0ZTFmNWYyZWMxYzEyNSIsInVzZXIiXX0sImlhdCI6MTY2ODgxMjQ1OH0.t1eQlRwkcP7v9JxUPMo3dcGKprH-uy8ujukNI7xE3A0"
+export const store = configureStore({
+  reducer: persistedReducer,//это combineReducers
+  middleware: (getDefaultMiddleware) =>
+    [...getDefaultMiddleware({ serializableCheck: { ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER] } }),
+    api.middleware],
+});
 
-store.dispatch(login(token));
-store.dispatch(logout());
-// store.dispatch(actionCategories());
-// // store.dispatch(actionFullLogin('test457', '123123'));
+const persistor = persistStore(store);
 
 export default store;

+ 0 - 24
js21 react/my-react-app/src/redux/reducers/promiseReducer.js

@@ -1,24 +0,0 @@
-export const promiseReducer = (state = {}, { key, type, status, payload, error }) => {
-  if (type === 'PROMISE') {
-    return { ...state, [key]: { status, payload, error } };
-  }
-  return state;
-}
-
-const actionPending = (key) => ({ key, type: 'PROMISE', status: 'PENDING' });
-const actionFulfilled = (key, payload) => ({ key, type: 'PROMISE', status: 'FULFILLED', payload });
-const actionRejected = (key, error) => ({ key, type: 'PROMISE', status: 'REJECTED', error });
-
-export const actionPromise = (key, promise) =>
-  async dispatch => {
-    dispatch(actionPending(key)); //сигнализируем redux, что промис начался
-    try {
-      const payload = await promise; //ожидаем промиса
-      dispatch(actionFulfilled(key, payload)); //сигнализируем redux, что промис успешно выполнен
-      return payload; //в месте запуска store.dispatch с этим thunk можно так же получить результат промиса
-    }
-    catch (error) {
-      dispatch(actionRejected(key, error)); //в случае ошибки - сигнализируем redux, что промис несложился
-      //main.innerHTML = '<div style="background-color:red; width:500px; height:500px"></div>';
-    }
-  }

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

@@ -1,55 +0,0 @@
-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) => {
-        state.status = 'loading';
-      })
-      .addCase(fetchCategories.fulfilled, (state, action) => {
-        state.status = 'succeeded';
-        state.categories = action.payload;
-      })
-      .addCase(fetchCategories.rejected, (state, action) => {
-        state.status = 'failed';
-        state.error = action.error.message;
-      })
-      .addCase(fetchCategoryById.pending, (state) => {
-        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;

+ 0 - 38
js21 react/my-react-app/src/redux/slices/goodSlice.js

@@ -1,38 +0,0 @@
-import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
-import { gqlGetGood } from '../../api/gql';
-
-const initialState = {
-  good: [],
-  status: 'idle',
-  error: null,
-};
-
-export const fetchGoodById = createAsyncThunk('good/fetchGoodById', async (id) => {
-  const response = await gqlGetGood(id);
-  return response;
-});
-
-const goodSlice = createSlice({
-  name: 'good',
-  initialState,
-  reducers: {},
-  extraReducers(builder) {
-    builder
-      .addCase(fetchGoodById.pending, (state) => {
-        state.status = 'loading';
-      })
-      .addCase(fetchGoodById.fulfilled, (state, action) => {
-        state.status = 'succeeded';
-        state.good = action.payload;
-      })
-      .addCase(fetchGoodById.rejected, (state, action) => {
-        state.status = 'failed';
-        state.error = action.error.message;
-      })
-  },
-});
-
-export default goodSlice.reducer;
-
-export const selectGood = (state) => state.good.good;
-export const selectGoodStatus = (state) => state.good.status;