Przeglądaj źródła

done hw redux gql with navigation

unknown 2 lat temu
commit
d2913838d6
76 zmienionych plików z 41644 dodań i 0 usunięć
  1. 1 0
      .eslintcache
  2. 26 0
      .gitignore
  3. 5 0
      .huskyrc
  4. 4 0
      .lintstagedrc
  5. 11 0
      .prettierrc.yaml
  6. 15 0
      README.md
  7. 8 0
      netlify.toml
  8. 39660 0
      package-lock.json
  9. 65 0
      package.json
  10. BIN
      public/favicon.ico
  11. 43 0
      public/index.html
  12. BIN
      public/logo192.png
  13. BIN
      public/logo512.png
  14. 25 0
      public/manifest.json
  15. 3 0
      public/robots.txt
  16. 116 0
      src/App.js
  17. 208 0
      src/api-data/index.js
  18. 15 0
      src/components/Authorization/Authorization.jsx
  19. 5 0
      src/components/Authorization/Authorization.module.css
  20. 72 0
      src/components/Authorization/Registration/RegistrationForm.jsx
  21. 55 0
      src/components/Authorization/Registration/RegistrationForm.module.css
  22. 72 0
      src/components/Authorization/SignIn/SignIn.jsx
  23. 54 0
      src/components/Authorization/SignIn/SignIn.module.css
  24. 10 0
      src/components/Authorization/helpers/helpers.js
  25. 26 0
      src/components/Authorization/helpers/validation.js
  26. 41 0
      src/components/Categories/Categories.jsx
  27. 16 0
      src/components/Categories/Categories.module.css
  28. 16 0
      src/components/Categories/Category/Category.jsx
  29. 19 0
      src/components/Categories/Category/Category.module.css
  30. 30 0
      src/components/Categories/DetailCategory/DetailCategory.jsx
  31. 16 0
      src/components/Categories/DetailCategory/DetailCategory.module.css
  32. 35 0
      src/components/Goods/DetailGood/DetailGood.jsx
  33. 18 0
      src/components/Goods/DetailGood/DetailGood.module.css
  34. 19 0
      src/components/Goods/Good/Good.jsx
  35. 17 0
      src/components/Goods/Good/Good.module.css
  36. 41 0
      src/components/Goods/Goods.jsx
  37. 15 0
      src/components/Goods/Goods.module.css
  38. 18 0
      src/components/Loader/Loader.jsx
  39. 23 0
      src/components/Loader/Loader.module.css
  40. 20 0
      src/components/Navigation/Button/Button.jsx
  41. 52 0
      src/components/Navigation/Button/Button.module.css
  42. 26 0
      src/components/Navigation/Navigation.jsx
  43. 22 0
      src/components/Navigation/Navigation.module.css
  44. 29 0
      src/components/Orders/DetailOrder/DetailOrder.jsx
  45. 16 0
      src/components/Orders/DetailOrder/DetailOrder.module.css
  46. 16 0
      src/components/Orders/Order/Order.jsx
  47. 19 0
      src/components/Orders/Order/Order.module.css
  48. 40 0
      src/components/Orders/Orders.jsx
  49. 16 0
      src/components/Orders/Orders.module.css
  50. 22 0
      src/components/PaginationBar/PaginationBar.jsx
  51. 6 0
      src/components/PaginationBar/PaginationBar.module.css
  52. 12 0
      src/components/Routes/PrivateRoute/PrivateRoute.js
  53. 18 0
      src/components/Routes/PublicRoute/PublicRoute.js
  54. 43 0
      src/index.css
  55. 22 0
      src/index.js
  56. 24 0
      src/redux/authorization/action/index.js
  57. 52 0
      src/redux/authorization/operations/index.js
  58. 27 0
      src/redux/authorization/reducer/index.js
  59. 4 0
      src/redux/authorization/selector/index.js
  60. 24 0
      src/redux/categories/action/index.js
  61. 36 0
      src/redux/categories/operations/index.js
  62. 29 0
      src/redux/categories/reducer/index.js
  63. 4 0
      src/redux/categories/selector/index.js
  64. 24 0
      src/redux/goods/action/index.js
  65. 36 0
      src/redux/goods/operations/index.js
  66. 29 0
      src/redux/goods/reducer/index.js
  67. 4 0
      src/redux/goods/selector/index.js
  68. 7 0
      src/redux/loading/action/index.js
  69. 7 0
      src/redux/loading/reducer/index.js
  70. 3 0
      src/redux/loading/selector/index.js
  71. 24 0
      src/redux/orders/action/index.js
  72. 36 0
      src/redux/orders/operations/index.js
  73. 29 0
      src/redux/orders/reducer/index.js
  74. 4 0
      src/redux/orders/selector/index.js
  75. 25 0
      src/redux/rootReducer/index.js
  76. 14 0
      src/redux/store/index.js

Plik diff jest za duży
+ 1 - 0
.eslintcache


+ 26 - 0
.gitignore

@@ -0,0 +1,26 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+
+# production
+# /build
+
+# misc
+.DS_Store
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# Local Netlify folder
+.netlify

+ 5 - 0
.huskyrc

@@ -0,0 +1,5 @@
+{
+  "hooks": {
+    "pre-commit": "lint-staged"
+  }
+}

+ 4 - 0
.lintstagedrc

@@ -0,0 +1,4 @@
+{
+  "src/**/*.{json,css,scss,md}": ["prettier --write"],
+  "src/**/*.{js,jsx,ts,tsx}": ["prettier --write", "eslint --fix"]
+}

+ 11 - 0
.prettierrc.yaml

@@ -0,0 +1,11 @@
+printWidth: 80
+tabWidth: 2
+useTabs: false
+semi: true
+singleQuote: true
+trailingComma: all
+bracketSpacing: true
+jsxBracketSameLine: false
+arrowParens: avoid
+proseWrap: always
+

+ 15 - 0
README.md

@@ -0,0 +1,15 @@
+# react-21-22
+
+- [Настраиваем линтинг перед коммитом](https://github.com/goitacademy/react-lint-config)
+- [Абсолютные импорты](https://create-react-app.dev/docs/importing-a-component/#absolute-imports)
+- Распыление пропсов на примере `PaintingList` и `Painting`
+- Нормализация стилей:
+  - [PostCSS Normalize](https://create-react-app.dev/docs/adding-css-reset)
+  - [modern-normalize](https://github.com/sindresorhus/modern-normalize)
+- Ванильный CSS и препроцессоры
+- Инлайн стили
+- CSS-модули
+  - Композиция с `composes`
+  - Переменные
+- CSS in JS
+- Про библиотеки компонентов

+ 8 - 0
netlify.toml

@@ -0,0 +1,8 @@
+[build]
+  publish = "build"
+
+  
+[[redirects]]
+from = "/*"
+to = "/index.html"
+status = 200

Plik diff jest za duży
+ 39660 - 0
package-lock.json


+ 65 - 0
package.json

@@ -0,0 +1,65 @@
+{
+  "name": "react-21-22",
+  "version": "0.1.0",
+  "private": true,
+  "dependencies": {
+    "@material-ui/core": "^4.11.3",
+    "@reduxjs/toolkit": "^1.5.0",
+    "@testing-library/jest-dom": "^5.11.6",
+    "@testing-library/react": "^11.2.2",
+    "@testing-library/user-event": "^12.2.2",
+    "axios": "^0.21.1",
+    "bootstrap": "^4.6.0",
+    "formik": "^2.2.6",
+    "gh-pages": "^3.1.0",
+    "modern-normalize": "^1.0.0",
+    "react": "^17.0.1",
+    "react-bootstrap": "^1.6.1",
+    "react-dom": "^17.0.1",
+    "react-js-pagination": "^3.0.3",
+    "react-loader-spinner": "^4.0.0",
+    "react-redux": "^7.2.2",
+    "react-router-dom": "^5.2.0",
+    "react-scripts": "4.0.1",
+    "react-toastify": "^6.2.0",
+    "redux": "^4.0.5",
+    "redux-devtools-extension": "^2.13.8",
+    "redux-logger": "^3.0.6",
+    "redux-persist": "^6.0.0",
+    "redux-toolkit": "^1.1.2",
+    "web-vitals": "^0.2.4",
+    "yup": "^0.32.8"
+  },
+  "scripts": {
+    "start": "set PORT=3003 && react-scripts start",
+    "build": "react-scripts build",
+    "test": "react-scripts test",
+    "eject": "react-scripts eject",
+    "predeploy": "npm run build",
+    "deploy": "netlify deploy -p"
+  },
+  "eslintConfig": {
+    "extends": [
+      "react-app",
+      "react-app/jest"
+    ]
+  },
+  "browserslist": {
+    "production": [
+      ">0.2%",
+      "not dead",
+      "not op_mini all"
+    ],
+    "development": [
+      "last 1 chrome version",
+      "last 1 firefox version",
+      "last 1 safari version"
+    ]
+  },
+  "devDependencies": {
+    "husky": "^4.3.0",
+    "lint-staged": "^10.5.2",
+    "prettier": "^2.2.1",
+    "prop-types": "^15.7.2"
+  }
+}

BIN
public/favicon.ico


+ 43 - 0
public/index.html

@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8" />
+    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
+    <meta name="viewport" content="width=device-width, initial-scale=1" />
+    <meta name="theme-color" content="#000000" />
+    <meta
+      name="description"
+      content="Web site created using create-react-app"
+    />
+    <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
+    <!--
+      manifest.json provides metadata used when your web app is installed on a
+      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
+    -->
+    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
+    <!--
+      Notice the use of %PUBLIC_URL% in the tags above.
+      It will be replaced with the URL of the `public` folder during the build.
+      Only files inside the `public` folder can be referenced from the HTML.
+
+      Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
+      work correctly both with client-side routing and a non-root public URL.
+      Learn how to configure a non-root public URL by running `npm run build`.
+    -->
+    <title>React App</title>
+  </head>
+  <body>
+    <noscript>You need to enable JavaScript to run this app.</noscript>
+    <div id="root"></div>
+    <!--
+      This HTML file is a template.
+      If you open it directly in the browser, you will see an empty page.
+
+      You can add webfonts, meta tags, or analytics to this file.
+      The build step will place the bundled scripts into the <body> tag.
+
+      To begin the development, run `npm start` or `yarn start`.
+      To create a production bundle, use `npm run build` or `yarn build`.
+    -->
+  </body>
+</html>

BIN
public/logo192.png


BIN
public/logo512.png


+ 25 - 0
public/manifest.json

@@ -0,0 +1,25 @@
+{
+  "short_name": "React App",
+  "name": "Create React App Sample",
+  "icons": [
+    {
+      "src": "favicon.ico",
+      "sizes": "64x64 32x32 24x24 16x16",
+      "type": "image/x-icon"
+    },
+    {
+      "src": "logo192.png",
+      "type": "image/png",
+      "sizes": "192x192"
+    },
+    {
+      "src": "logo512.png",
+      "type": "image/png",
+      "sizes": "512x512"
+    }
+  ],
+  "start_url": ".",
+  "display": "standalone",
+  "theme_color": "#000000",
+  "background_color": "#ffffff"
+}

+ 3 - 0
public/robots.txt

@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:

+ 116 - 0
src/App.js

@@ -0,0 +1,116 @@
+import { lazy, Suspense } from 'react';
+import { BrowserRouter, Switch } from 'react-router-dom';
+import 'react-toastify/dist/ReactToastify.css';
+import { ToastContainer } from 'react-toastify';
+
+import Loader from './components/Loader/Loader';
+import Navigation from './components/Navigation/Navigation';
+import PrivateRoute from './components/Routes/PrivateRoute/PrivateRoute';
+import PublicRoute from './components/Routes/PublicRoute/PublicRoute';
+
+const Categories = lazy(() =>
+  import(
+    './components/Categories/Categories' /* webpackChunkName: "CategoriesPage" */
+  ),
+);
+
+const DetailCategory = lazy(() =>
+  import(
+    './components/Categories/DetailCategory/DetailCategory' /* webpackChunkName: "DetailCategory" */
+  ),
+);
+
+const Goods = lazy(() =>
+  import('./components/Goods/Goods' /* webpackChunkName: "GoodsPage" */),
+);
+
+const DetailGood = lazy(() =>
+  import(
+    './components/Goods/DetailGood/DetailGood' /* webpackChunkName: "DetailGood" */
+  ),
+);
+
+const Orders = lazy(() =>
+  import('./components/Orders/Orders' /* webpackChunkName: "OrdersPage" */),
+);
+
+const DetailOrder = lazy(() =>
+  import(
+    './components/Orders/DetailOrder/DetailOrder' /* webpackChunkName: "DetailOrder" */
+  ),
+);
+
+const Authorization = lazy(() =>
+  import(
+    './components/Authorization/Authorization' /* webpackChunkName: "AuthorizationPage" */
+  ),
+);
+
+const SignInForm = lazy(() =>
+  import(
+    './components/Authorization/SignIn/SignIn' /* webpackChunkName: "SignInForm" */
+  ),
+);
+
+const RegistrationForm = lazy(() =>
+  import(
+    './components/Authorization/Registration/RegistrationForm' /* webpackChunkName: "RegistrationForm" */
+  ),
+);
+
+function App() {
+  return (
+    <Suspense fallback={<Loader />}>
+      <BrowserRouter>
+        <Switch>
+          <PrivateRoute exact path={'/'}>
+            <Navigation />
+          </PrivateRoute>
+          <PrivateRoute exact path="/categories">
+            <Navigation />
+            <Categories />
+          </PrivateRoute>
+          <PrivateRoute path="/categories/:id">
+            <DetailCategory />
+          </PrivateRoute>
+          <PrivateRoute exact path="/goods">
+            <Navigation />
+            <Goods />
+          </PrivateRoute>
+          <PrivateRoute path="/goods/:id">
+            <DetailGood />
+          </PrivateRoute>
+          <PrivateRoute exact path="/orders">
+            <Navigation />
+            <Orders />
+          </PrivateRoute>
+          <PrivateRoute path="/orders/:id">
+            <DetailOrder />
+          </PrivateRoute>
+          <PublicRoute path={'/authorization'} restricted>
+            <Authorization />
+            <PublicRoute exact path={'/authorization/login'} restricted>
+              <SignInForm />
+            </PublicRoute>
+            <PublicRoute exact path={'/authorization/registration'} restricted>
+              <RegistrationForm />
+            </PublicRoute>
+          </PublicRoute>
+        </Switch>
+        <ToastContainer
+          position="top-right"
+          autoClose={3000}
+          hideProgressBar={false}
+          newestOnTop={false}
+          closeOnClick
+          rtl={false}
+          pauseOnFocusLoss
+          draggable
+          pauseOnHover
+        />
+      </BrowserRouter>
+    </Suspense>
+  );
+}
+
+export default App;

+ 208 - 0
src/api-data/index.js

@@ -0,0 +1,208 @@
+const getGQL = url => async (query, variables) => {
+  try {
+    const token = localStorage.token;
+    const res = await fetch(url, {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+        Authorization: 'Bearer ' + token ? token : '',
+      },
+      body: JSON.stringify({ query, variables }),
+    });
+    return res.json();
+  } catch (e) {
+    return e;
+  }
+};
+
+const gql = getGQL('http://shop-roles.asmer.fs.a-level.com.ua/graphql');
+
+const loginGQL = async (login, password) => {
+  try {
+    const { data } = await gql(
+      `
+query log($login:String, $password:String){
+        login(login:$login, password:$password)
+    }`,
+      { login, password },
+    );
+    const token = data.login;
+    if (token) localStorage.token = token;
+    return token;
+  } catch (e) {
+    console.error(e);
+  }
+};
+
+const registerGQL = async (login, password) => {
+  try {
+    const { data } = await gql(
+      `mutation register($login:String, $password:String){
+        UserUpsert(user: {login:$login, password:$password}){
+			_id,login
+		}
+    }`,
+      { login, password },
+    );
+    return data.UserUpsert;
+  } catch (e) {
+    console.error(e);
+  }
+};
+
+const categoriesGQL = async () => {
+  try {
+    const { data } = await gql(
+      `query allCategories{
+		CategoryFind(query:"[{}]"){
+          _id,
+          name,
+		  createdAt
+		}
+	}`,
+      {},
+    );
+    return data.CategoryFind;
+  } catch (e) {
+    console.error(e);
+  }
+};
+
+const goodsGQL = async () => {
+  try {
+    const { data } = await gql(
+      `query allGoods{
+		GoodFind(query:"[{}]"){
+          _id
+          name,
+		  createdAt,
+		  price,
+      images  {
+				url
+			}
+		}
+	}`,
+      {},
+    );
+    return data.GoodFind;
+  } catch (e) {
+    console.error(e);
+  }
+};
+
+const categoryById = async _id => {
+  try {
+    const { data } = await gql(
+      `query categoryById($id:String){
+		CategoryFindOne(query:$id){
+			_id,name,createdAt,
+			goods {
+				_id,createdAt,name
+			}
+		 },
+	}`,
+      { id: JSON.stringify([{ _id }]) },
+    );
+    return data.CategoryFindOne;
+  } catch (e) {
+    console.error(e);
+  }
+};
+
+const goodById = async _id => {
+  try {
+    const { data } = await gql(
+      `query findById($id:String){
+        GoodFindOne (query:$id){
+            _id createdAt name price description images  {
+				url
+			}
+        }
+    }`,
+      { id: JSON.stringify([{ _id }]) },
+    );
+
+    return data.GoodFindOne;
+  } catch (e) {
+    console.error(e);
+  }
+};
+
+const ordersGQL = async () => {
+  try {
+    const { data } = await gql(
+      `query orders {
+    OrderFind(query:"[{}]")
+    {
+    _id
+    createdAt 
+    total
+    orderGoods {
+      price
+      count
+      total
+      good {
+        name
+      }
+    }
+  }
+}`,
+      {},
+    );
+    return data.OrderFind;
+  } catch (e) {
+    console.error(e);
+  }
+};
+
+const orderById = async _id => {
+  try {
+    const { data } = await gql(
+      `query findById($id:String){
+        OrderFindOne (query:$id){
+            _id createdAt name price description images  {
+				url
+			}
+        }
+    }`,
+      { id: JSON.stringify([{ _id }]) },
+    );
+
+    return data.OrderFindOne;
+  } catch (e) {
+    console.error(e);
+  }
+};
+
+const makeOrderById = async id => {
+  try {
+    const { data } = await gql(
+      `  mutation makeOrder($id:ID){
+		OrderUpsert(order:{
+      orderGoods:[
+        {count:1,good:{_id:$id}}
+      ]
+    }){
+          _id,createdAt,total
+		}
+	}`,
+      { id },
+    );
+
+    return data.OrderUpsert;
+  } catch (e) {
+    console.error(e);
+  }
+};
+
+export {
+  loginGQL,
+  registerGQL,
+  categoriesGQL,
+  goodsGQL,
+  categoryById,
+  goodById,
+  ordersGQL,
+  orderById,
+  makeOrderById,
+};

+ 15 - 0
src/components/Authorization/Authorization.jsx

@@ -0,0 +1,15 @@
+import s from './Authorization.module.css';
+import Button from '../Navigation/Button/Button';
+
+
+
+const Authorization = () => {
+    return (
+        <div className={s.authorizationWrapper}>
+            <Button text='Login' to='/authorization/login' />
+            <Button text='Registration' to='/authorization/registration' />
+      </div>
+  );
+};
+
+export default Authorization;

+ 5 - 0
src/components/Authorization/Authorization.module.css

@@ -0,0 +1,5 @@
+.authorizationWrapper{
+    display: flex;
+    justify-content: center;
+    padding: 20px 0;
+}

+ 72 - 0
src/components/Authorization/Registration/RegistrationForm.jsx

@@ -0,0 +1,72 @@
+import React from 'react';
+import { useFormik } from 'formik';
+import { useDispatch, useSelector } from 'react-redux';
+import Button from '@material-ui/core/Button';
+import TextField from '@material-ui/core/TextField';
+
+import { getLoad } from '../../../redux/loading/selector';
+import { validationSchemaRegistration } from '../helpers/validation';
+import { asyncCreateUser } from '../../../redux/authorization/operations';
+import  togglePassword  from '../helpers/helpers';
+import s from './RegistrationForm.module.css';
+
+const RegistrationForm = () => {
+  const dispatch = useDispatch();
+  const isLoading = useSelector(getLoad);
+
+  const formik = useFormik({
+    initialValues: {
+      email: '',
+      password: '',
+    },
+    validationSchema: validationSchemaRegistration,
+    onSubmit: ({email:login,password}) => dispatch( asyncCreateUser(login,password)),
+  });
+
+  return (
+    <div>
+      <form className={s.registration_form} onSubmit={formik.handleSubmit}>
+        <div className={s.registration_input_wrapper}>
+          <TextField
+            id="email"
+            name="email"
+            label="Email..."
+            value={formik.values.email}
+            onChange={formik.handleChange}
+            error={formik.touched.email && Boolean(formik.errors.email)}
+            helperText={formik.touched.email && formik.errors.email}
+          />
+          <TextField
+            id="password"
+            name="password"
+            label="Password..."
+            type="password"
+            value={formik.values.password}
+            onChange={formik.handleChange}
+            error={formik.touched.password && Boolean(formik.errors.password)}
+            helperText={formik.touched.password && formik.errors.password}
+          />
+          <div>
+            <input
+              className={s.registration_show_password}
+              type="checkbox"
+              onClick={togglePassword}
+            />
+            Show password*
+          </div>
+        </div>
+        <Button
+          className={s.registration_button}
+          color="primary"
+          variant="contained"
+          disabled={isLoading ? true : false}
+          type="submit"
+        >
+          Submit
+        </Button>
+      </form>
+    </div>
+  );
+};
+
+export default RegistrationForm;

+ 55 - 0
src/components/Authorization/Registration/RegistrationForm.module.css

@@ -0,0 +1,55 @@
+.registration_form {
+  width: 360px;
+  min-height: 106px;
+  display: flex;
+  height: auto;
+  background-color: rgb(194, 192, 196);
+  margin: 0 auto;
+  -webkit-box-shadow: 0px -23px 30px 18px #7c3ee0;
+  box-shadow: 0px -23px 30px 18px #858586;
+  border: solid 3px rgb(24, 24, 24);
+  border-radius: 20px;
+  padding-bottom: 10px;
+  padding-left: 6px;
+  padding-top: 3px;
+  padding-right: 2px;
+  border-bottom: none;
+  margin-top: 100px;
+}
+
+.registration_input_wrapper {
+  display: flex;
+  flex-direction: column;
+  justify-content: space-between;
+  padding-right: 10px;
+  width: 280px;
+}
+
+
+
+.registration_button {
+  text-orientation: upright;
+  writing-mode: vertical-rl;
+  transform: rotate(7deg);
+  -ms-transform: rotate(7deg);
+  -ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.93969262, M12=0.34202014, M21=-0.34202014, M22=0.93969262,sizingMethod='auto expand')"; /* IE6-8 */
+  -webkit-transform: rotate(7deg);
+  width: 64px;
+  height: auto;
+}
+
+.registration_show_password {
+  display: inline;
+  margin-right: 10px;
+}
+
+@media (min-width: 767px) {
+  .registration_form {
+    width: 600px;
+  }
+
+  .registration_input_wrapper {
+    padding-right: 10px;
+    width: 550px;
+  }
+}

+ 72 - 0
src/components/Authorization/SignIn/SignIn.jsx

@@ -0,0 +1,72 @@
+import React from 'react';
+import { useFormik } from 'formik';
+import { useDispatch, useSelector } from 'react-redux';
+import Button from '@material-ui/core/Button';
+import TextField from '@material-ui/core/TextField';
+
+import { getLoad } from '../../../redux/loading/selector';
+import { validationSchemaSignIn } from '../helpers/validation';
+import { asyncLogin } from '../../../redux/authorization/operations';
+import  togglePassword  from '../helpers/helpers';
+import s from './SignIn.module.css';
+
+const SignInForm = () => {
+  const dispatch = useDispatch();
+  const isLoading = useSelector(getLoad);
+
+  const formik = useFormik({
+    initialValues: {
+      email: '',
+      password: '',
+    },
+    validationSchema: validationSchemaSignIn,
+    onSubmit: ({email:login,password}) => dispatch(asyncLogin(login,password)),
+  });
+
+  return (
+    <form className={s.signIn_form} onSubmit={formik.handleSubmit}>
+      <div className={s.signIn_input_wrapper}>
+        <TextField
+          className={s.signIn_input}
+          id="email"
+          name="email"
+          label="Email..."
+          value={formik.values.email}
+          onChange={formik.handleChange}
+          error={formik.touched.email && Boolean(formik.errors.email)}
+          helperText={formik.touched.email && formik.errors.email}
+        />
+        <div>
+          <TextField
+            className={s.signIn_input}
+            id="password"
+            name="password"
+            label="Password..."
+            type="password"
+            value={formik.values.password}
+            onChange={formik.handleChange}
+            error={formik.touched.password && Boolean(formik.errors.password)}
+            helperText={formik.touched.password && formik.errors.password}
+          />
+          <input
+            className={s.input_show_password}
+            type="checkbox"
+            onClick={togglePassword}
+          />
+          Show password*
+        </div>
+      </div>
+      <Button
+        className={s.signIn_button}
+        color="primary"
+        variant="contained"
+        type="submit"
+        disabled={isLoading ? true : false}
+      >
+        Submit
+      </Button>
+    </form>
+  );
+};
+
+export default SignInForm;

+ 54 - 0
src/components/Authorization/SignIn/SignIn.module.css

@@ -0,0 +1,54 @@
+.signIn_form {
+  width: 360px;
+  min-height: 106px;
+  display: flex;
+  height: auto;
+  background-color: rgb(164, 162, 167);
+  margin: 0 auto;
+  -webkit-box-shadow: 0px -23px 30px 18px #bebbc4;
+  box-shadow: 0px -23px 30px 18px #c1c0c2;
+  border: solid 3px rgb(2, 2, 2);
+  border-radius: 20px;
+  padding-bottom: 10px;
+  padding-left: 6px;
+  padding-top: 3px;
+  padding-right: 2px;
+  border-bottom: none;
+  margin-top: 100px;
+}
+.signIn_input {
+  width: 100%;
+}
+.signIn_input_wrapper {
+  display: flex;
+  flex-direction: column;
+  justify-content: space-between;
+  padding-right: 10px;
+  width: 280px;
+}
+.signIn_button {
+  text-orientation: upright;
+  writing-mode: vertical-rl;
+  transform: rotate(7deg);
+  -ms-transform: rotate(7deg);
+  -ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.93969262, M12=0.34202014, M21=-0.34202014, M22=0.93969262,sizingMethod='auto expand')"; /* IE6-8 */
+  -webkit-transform: rotate(7deg);
+  width: 64px;
+  height: auto;
+}
+
+.input_show_password {
+  display: inline-block;
+  margin-right: 10px;
+}
+
+@media (min-width: 767px) {
+  .signIn_form {
+    width: 600px;
+  }
+
+  .signIn_input_wrapper {
+    padding-right: 10px;
+    width: 550px;
+  }
+}

+ 10 - 0
src/components/Authorization/helpers/helpers.js

@@ -0,0 +1,10 @@
+const togglePassword = () => {
+  let el = document.getElementById('password');
+  if (el.type === 'password') {
+    el.type = 'text';
+  } else {
+    el.type = 'password';
+  }
+};
+
+export default togglePassword;

+ 26 - 0
src/components/Authorization/helpers/validation.js

@@ -0,0 +1,26 @@
+import * as yup from 'yup';
+
+const validationSchemaRegistration = yup.object({
+  email: yup
+    .string('Enter your email')
+    .email('Enter a valid email')
+    .required('Email is required'),
+  password: yup
+    .string('Enter your password')
+    .min(8, 'Password should be of minimum 8 characters length')
+    .required('Password is required'),
+});
+
+const validationSchemaSignIn = yup.object({
+  email: yup
+    .string('Enter your email')
+    .email('Enter a valid email')
+    .required('Email is required'),
+  password: yup
+    .string('Enter your password')
+    .min(8, 'Password should be of minimum 8 characters length')
+    .max(20, 'Password should be less 20 characters')
+    .required('Password is required'),
+});
+
+export { validationSchemaRegistration, validationSchemaSignIn };

+ 41 - 0
src/components/Categories/Categories.jsx

@@ -0,0 +1,41 @@
+import { useDispatch,useSelector } from 'react-redux';
+import { useEffect, useState } from 'react';
+import { Route } from 'react-router-dom';
+
+
+import s from './Categories.module.css';
+import { asyncGetCategories } from '../../redux/categories/operations';
+import {getCategories} from '../../redux/categories/selector';
+
+import Category from './Category/Category';
+import Pagination from '../PaginationBar/PaginationBar';
+
+const Categories = () => {
+  const dispatch = useDispatch();
+  const categories = useSelector(getCategories);
+  const [page, setPage] = useState(1);
+ 
+
+  const handlePageChange = (page) => setPage(page)
+
+   useEffect(() => {
+     dispatch(asyncGetCategories())
+   }, [dispatch]);
+
+
+  return (
+    <Route>
+      <ul className={s.categories_list}>
+        {categories.length !== 0 && categories.slice((page-1)*5,(page*5)).map((category,i) => <Category key={i} category={category} />)}
+      </ul>
+        <Pagination
+          page={page}
+          total={Math.ceil(categories.length/5)}
+          handlePageChange={handlePageChange}
+        />
+
+    </Route>
+  );
+};
+
+export default Categories;

+ 16 - 0
src/components/Categories/Categories.module.css

@@ -0,0 +1,16 @@
+.categories_list {
+  font-family: 'Mountains of Christmas', cursive;
+  list-style: none;
+  display: flex;
+  justify-content: space-around;
+  flex-wrap: wrap;
+  padding-bottom: 10px;
+  padding-top: 100px;
+  padding-left: 0;
+}
+
+@media (min-width: 767px) {
+}
+
+@media (min-width: 1400px) {
+}

+ 16 - 0
src/components/Categories/Category/Category.jsx

@@ -0,0 +1,16 @@
+import React from 'react';
+import { Link } from 'react-router-dom';
+
+import s from './Category.module.css';
+
+const Category = ({ category: { _id, name, createdAt } }) => {
+      return (
+        <li  className={s.category_item}>
+          <Link className={s.category_link} to={`/categories/${_id}`}>
+            <p>name : {name?name:'missed'}</p>
+            <p>createdAt : {createdAt?createdAt:'missed'}</p>
+          </Link>
+        </li>
+      );
+};
+export default Category;

+ 19 - 0
src/components/Categories/Category/Category.module.css

@@ -0,0 +1,19 @@
+.category_item {
+  position: relative;
+  margin-bottom: 10px;
+}
+
+.category_link {
+  text-decoration: none;
+  color: white;
+}
+
+
+
+@media (min-width: 767px) {
+ 
+}
+
+@media (min-width: 1400px) {
+ 
+}

+ 30 - 0
src/components/Categories/DetailCategory/DetailCategory.jsx

@@ -0,0 +1,30 @@
+import { useEffect } from 'react';
+import { useRouteMatch } from 'react-router-dom';
+import { useDispatch,useSelector } from 'react-redux';
+
+
+import s from './DetailCategory.module.css';
+import { asyncGetCategoryById } from '../../../redux/categories/operations';
+import { getCategory} from '../../../redux/categories/selector';
+
+function DetailCategory() {
+  const { params:{id} } = useRouteMatch();
+  const dispatch = useDispatch();
+  const category = useSelector(getCategory);
+
+
+  useEffect(() => {
+    dispatch(asyncGetCategoryById(id))
+  }, [dispatch, id]);
+
+
+  return (
+      category.length !== 0 &&(
+        <div className={s.detailCategory_wrapper}>
+          <p>name : {category[0].name?category[0].name:'missed'}</p>
+          <p>createdAt : {category[0].createdAt?category[0].createdAt:'missed'}</p>
+        </div>
+      )
+  )
+}
+export default DetailCategory;

+ 16 - 0
src/components/Categories/DetailCategory/DetailCategory.module.css

@@ -0,0 +1,16 @@
+.detailCategory_wrapper {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex-direction: column;
+  padding: 10px;
+  padding-top: 60px;
+  color: white;
+  font-size: 24px;
+}
+
+
+@media (min-width: 767px) {
+}
+@media (min-width: 1400px) {
+}

+ 35 - 0
src/components/Goods/DetailGood/DetailGood.jsx

@@ -0,0 +1,35 @@
+import { useEffect } from 'react';
+import { useRouteMatch } from 'react-router-dom';
+import { useDispatch,useSelector } from 'react-redux';
+
+
+
+
+import s from './DetailGood.module.css';
+import { asyncGetGoodById } from '../../../redux/goods/operations';
+import { getGood} from '../../../redux/goods/selector';
+
+function DetailCategory() {
+  const { params:{id} } = useRouteMatch();
+  const dispatch = useDispatch();
+  const good = useSelector(getGood);
+
+
+  useEffect(() => {
+    dispatch(asyncGetGoodById(id))
+  }, [dispatch, id]);
+
+
+  return (
+      good.length !== 0 &&(
+        <div className={s.detailGood_wrapper}>
+        <p>name : {good[0].name?good[0].name:'missed'}</p>
+        <p>createdAt : {good[0].createdAt?good[0].createdAt:'missed'}</p>
+        <p>description : {good[0].description?good[0].description:'missed'}</p>
+        <img src={`http://shop-roles.asmer.fs.a-level.com.ua/${good[0].images&&good[0].images[0].url}`}
+          alt='*it has to be pic of product*' width='200' height='200'></img>
+        </div>
+      )
+  )
+}
+export default DetailCategory;

+ 18 - 0
src/components/Goods/DetailGood/DetailGood.module.css

@@ -0,0 +1,18 @@
+.detailGood_wrapper {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex-direction: column;
+  padding: 10px;
+  padding-top: 60px;
+  color: white;
+  font-size: 24px;
+}
+
+
+
+
+@media (min-width: 767px) {
+}
+@media (min-width: 1400px) {
+}

+ 19 - 0
src/components/Goods/Good/Good.jsx

@@ -0,0 +1,19 @@
+import React from 'react';
+import { Link } from 'react-router-dom';
+
+import s from './Good.module.css';
+
+const Good = ({ good: { _id, name, createdAt, price, images } }) => {
+  return (
+      <li  className={s.good_item}>
+        <Link className={s.good_link} to={`/goods/${_id}`}>
+        <p>name : {name?name:'missed'}</p>
+        <p>createdAt : {createdAt?createdAt:'missed'}</p>
+        <p>price : {price ? price : 'unknown'}</p>
+        <img src={`http://shop-roles.asmer.fs.a-level.com.ua/${images&&images[0].url}`}
+          alt='*it has to be pic of product*' width='200' height='200'></img>
+        </Link>
+      </li>
+      );
+};
+export default Good;

+ 17 - 0
src/components/Goods/Good/Good.module.css

@@ -0,0 +1,17 @@
+.good_item {
+  position: relative;
+  display: block;
+  margin-bottom: 10px;
+  width: 16%;
+  height: 300px;
+}
+
+.good_link {
+  text-decoration: none;
+  color: white;
+}
+
+@media (min-width: 767px) {
+}
+@media (min-width: 1400px) {
+}

+ 41 - 0
src/components/Goods/Goods.jsx

@@ -0,0 +1,41 @@
+import { useDispatch,useSelector } from 'react-redux';
+import { useEffect, useState } from 'react';
+import { Route } from 'react-router-dom';
+
+
+import s from './Goods.module.css';
+import { asyncGetGoods } from '../../redux/goods/operations';
+import {getGoods} from '../../redux/goods/selector';
+
+import Good from './Good/Good';
+import Pagination from '../PaginationBar/PaginationBar';
+
+const Goods = () => {
+  const dispatch = useDispatch();
+  const goods = useSelector(getGoods);
+  const [page, setPage] = useState(1);
+ 
+
+  const handlePageChange = (page) => setPage(page)
+
+   useEffect(() => {
+     dispatch(asyncGetGoods())
+   }, [dispatch]);
+
+
+  return (
+    <Route>
+      <ul className={s.goods_list}>
+        {goods.length !== 0 && goods.slice((page-1)*5,(page*5)).map((good,i) => <Good key={i} good={good} />)}
+      </ul>
+        <Pagination
+          page={page}
+          total={Math.ceil(goods.length/5)}
+          handlePageChange={handlePageChange}
+        />
+
+    </Route>
+  );
+};
+
+export default Goods;

+ 15 - 0
src/components/Goods/Goods.module.css

@@ -0,0 +1,15 @@
+.goods_list {
+  font-family: 'Mountains of Christmas', cursive;
+  list-style: none;
+  display: flex;
+  justify-content: space-around;
+  flex-wrap: wrap;
+  padding-bottom: 10px;
+  padding-top: 100px;
+  padding-left: 0;
+}
+
+@media (min-width: 767px) {
+}
+@media (min-width: 1400px) {
+}

+ 18 - 0
src/components/Loader/Loader.jsx

@@ -0,0 +1,18 @@
+import 'react-loader-spinner/dist/loader/css/react-spinner-loader.css';
+import Loader from 'react-loader-spinner';
+
+import s from './Loader.module.css';
+
+const Load = () => {
+  return (
+    <Loader
+      className={s.loader}
+      type="Puff"
+      color="#0ca0f5"
+      height={100}
+      width={100}
+      timeout={300000}
+    />
+  );
+};
+export default Load;

+ 23 - 0
src/components/Loader/Loader.module.css

@@ -0,0 +1,23 @@
+.loader {
+  position: fixed;
+  display: inline-block;
+  width: 5vw;
+  height: 5vh;
+  top: 86vh;
+  left: 0;
+  right: 0;
+  margin-left: auto;
+  margin-right: 20vw;
+}
+
+@media (min-width: 767px) {
+  .loader {
+    margin-right: 6vw;
+  }
+}
+
+@media (min-width: 1400px) {
+  .loader {
+    margin-right: 3vw;
+  }
+}

+ 20 - 0
src/components/Navigation/Button/Button.jsx

@@ -0,0 +1,20 @@
+import { NavLink } from 'react-router-dom';
+
+import s from './Button.module.css';
+
+const Button = ({text, to }) => {
+  return (
+    <button  type="button" className={s.button_navigation}>
+      <NavLink
+        exact
+        to={to}
+        activeClassName={s.navigation_link_active}
+        className={s.navigation_link}
+      >
+        {text}
+      </NavLink>
+    </button>
+  );
+};
+
+export default Button;

+ 52 - 0
src/components/Navigation/Button/Button.module.css

@@ -0,0 +1,52 @@
+.button_navigation {
+  background-color: #b3b3b3;
+  transition: all 250ms cubic-bezier(0.4, 0, 0.2, 1);
+  font-family: 'Mountains of Christmas', cursive;
+  text-align: center;
+  display: flex;
+  justify-content: center;
+  justify-items: center;
+  text-align: center;
+  cursor: pointer;
+  width: 80px;
+  font-size: 16px;
+  height: auto;
+  box-shadow: 0px 3px 1px -2px rgba(0, 0, 0, 0.2),
+    0px 2px 2px 0px rgba(0, 0, 0, 0.14), 0px 1px 5px 0px rgba(0, 0, 0, 0.12);
+}
+.button_navigation {
+  margin-right: 2%;
+}
+
+
+
+.navigation_link {
+  display: block;
+  width: 72px;
+  text-decoration: none;
+  color: rgb(255, 255, 255);
+}
+
+.navigation_link_active {
+  color: rgb(37, 156, 253);
+}
+
+@media (min-width: 767px) {
+  .button_navigation {
+    width: 100px;
+    font-size: 18px;
+  }
+  .navigation_link {
+    width: 90px;
+  }
+}
+
+@media (min-width: 1400px) {
+  .button_navigation {
+    width: 120px;
+    font-size: 20px;
+  }
+  .navigation_link {
+    width: 110px;
+  }
+}

+ 26 - 0
src/components/Navigation/Navigation.jsx

@@ -0,0 +1,26 @@
+import { useDispatch } from 'react-redux';
+
+import Button from './Button/Button';
+import { asyncLogout } from '../../redux/authorization/operations';
+
+
+import s from './Navigation.module.css';
+
+const Navigation = () => {
+  const dispatch = useDispatch();
+  const handleLogout = async () => dispatch(asyncLogout())
+
+  return (
+    <div className={s.wrapper__button}>
+      <Button  text="Categories" to="/categories" />
+      <Button  text="Goods" to="/goods" />
+      <Button  text="Orders" to="/orders" />
+      <div className={s.wrapper__button__logout}>
+        <div onClick={handleLogout}>
+          <Button  text="Logout" to="/authorization" />
+        </div>
+      </div>
+   </div>
+  );
+};
+export default Navigation;

+ 22 - 0
src/components/Navigation/Navigation.module.css

@@ -0,0 +1,22 @@
+.wrapper__button {
+  width: 100%;
+  position: fixed;
+  background-color: rgb(95, 92, 92);
+  width: 100vw;
+  z-index: 100;
+  display: flex;
+  justify-content: center;
+  padding: 10px 0;
+}
+
+.wrapper__button__logout{
+  width: 50%;
+  display: flex;
+  justify-content: flex-end;
+}
+
+@media (min-width: 767px) {
+  .wrapper__button {
+    padding: 15px 0;
+  }
+}

+ 29 - 0
src/components/Orders/DetailOrder/DetailOrder.jsx

@@ -0,0 +1,29 @@
+import { useEffect } from 'react';
+import { useRouteMatch } from 'react-router-dom';
+import { useDispatch,useSelector } from 'react-redux';
+
+
+import s from './DetailOrder.module.css';
+import { asyncGetOrderById } from '../../../redux/orders/operations';
+import { getOrder} from '../../../redux/orders/selector';
+
+function DetailOrder() {
+  const { params:{id} } = useRouteMatch();
+  const dispatch = useDispatch();
+  const order = useSelector(getOrder);
+
+
+  useEffect(() => {
+    dispatch(asyncGetOrderById(id))
+  }, [dispatch, id]);
+
+  return (
+      order.length !== 0 &&(
+        <div className={s.detailOrder_wrapper}>
+          <p>name : {order[0].name?order[0].name:'missed'}</p>
+          <p>createdAt : {order[0].createdAt?order[0].createdAt:'missed'}</p>
+        </div>
+      )
+  )
+}
+export default DetailOrder;

+ 16 - 0
src/components/Orders/DetailOrder/DetailOrder.module.css

@@ -0,0 +1,16 @@
+.detailOrder_wrapper {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex-direction: column;
+  padding: 10px;
+  padding-top: 60px;
+  color: white;
+  font-size: 24px;
+}
+
+
+@media (min-width: 767px) {
+}
+@media (min-width: 1400px) {
+}

+ 16 - 0
src/components/Orders/Order/Order.jsx

@@ -0,0 +1,16 @@
+import React from 'react';
+import { Link } from 'react-router-dom';
+
+import s from './Order.module.css';
+
+const Order = ({ order: { _id, total, createdAt } }) => {
+      return (
+        <li  className={s.order_item}>
+          <Link className={s.order_link} to={`/orders/${_id}`}>
+            <p>total : {total?total:'missed'}</p>
+            <p>createdAt : {createdAt?createdAt:'missed'}</p>
+          </Link>
+        </li>
+      );
+};
+export default Order;

+ 19 - 0
src/components/Orders/Order/Order.module.css

@@ -0,0 +1,19 @@
+.order_item {
+  position: relative;
+  margin-bottom: 10px;
+}
+
+.order_link {
+  text-decoration: none;
+  color: white;
+}
+
+
+
+@media (min-width: 767px) {
+ 
+}
+
+@media (min-width: 1400px) {
+ 
+}

+ 40 - 0
src/components/Orders/Orders.jsx

@@ -0,0 +1,40 @@
+import { useDispatch,useSelector } from 'react-redux';
+import { useEffect, useState } from 'react';
+import { Route } from 'react-router-dom';
+
+
+import s from './Orders.module.css';
+import { asyncGetOrders } from '../../redux/orders/operations';
+import {getOrders} from '../../redux/orders/selector';
+
+import Order from './Order/Order';
+import Pagination from '../PaginationBar/PaginationBar';
+
+const Orders = () => {
+  const dispatch = useDispatch();
+  const orders = useSelector(getOrders);
+  const [page, setPage] = useState(1);
+ 
+
+  const handlePageChange = (page) => setPage(page)
+
+   useEffect(() => {
+     dispatch(asyncGetOrders())
+   }, [dispatch]);
+
+  return (
+    <Route>
+      <ul className={s.orders_list}>
+        {orders.length !== 0 && orders.slice((page-1)*5,(page*5)).map((order,i) => <Order key={i} order={order} />)}
+      </ul>
+        <Pagination
+          page={page}
+          total={Math.ceil(orders.length/5)}
+          handlePageChange={handlePageChange}
+        />
+
+    </Route>
+  );
+};
+
+export default Orders;

+ 16 - 0
src/components/Orders/Orders.module.css

@@ -0,0 +1,16 @@
+.orders_list {
+  font-family: 'Mountains of Christmas', cursive;
+  list-style: none;
+  display: flex;
+  justify-content: space-around;
+  flex-wrap: wrap;
+  padding-bottom: 10px;
+  padding-top: 100px;
+  padding-left: 0;
+}
+
+@media (min-width: 767px) {
+}
+
+@media (min-width: 1400px) {
+}

+ 22 - 0
src/components/PaginationBar/PaginationBar.jsx

@@ -0,0 +1,22 @@
+import Pagination from 'react-js-pagination';
+import 'bootstrap/dist/css/bootstrap.min.css';
+
+import s from './PaginationBar.module.css';
+
+function PaginationBar({ page, total, handlePageChange }) {
+  return (
+    <div className={s.pagination_wrapper}>
+      <Pagination
+        itemClass="page-item"
+        linkClass="page-link"
+        activePage={page}
+        itemsCountPerPage={1}
+        totalItemsCount={total}
+        pageRangeDisplayed={total > 5 ? 5 : total}
+        onChange={handlePageChange}
+      />
+    </div>
+  );
+}
+
+export default PaginationBar;

+ 6 - 0
src/components/PaginationBar/PaginationBar.module.css

@@ -0,0 +1,6 @@
+.pagination_wrapper {
+  margin: 0 auto;
+  padding-bottom: 15px;
+  display: flex;
+  justify-content: center;
+}

+ 12 - 0
src/components/Routes/PrivateRoute/PrivateRoute.js

@@ -0,0 +1,12 @@
+import { useSelector } from 'react-redux';
+import { Route, Redirect } from 'react-router-dom';
+import { getToken } from '../../../redux/authorization/selector';
+
+export default function PrivateRoute({ children, ...routeProps }) {
+  const token = useSelector(getToken);
+  return (
+    <Route {...routeProps}>
+      {token ? children : <Redirect to="/authorization" />}
+    </Route>
+  );
+}

+ 18 - 0
src/components/Routes/PublicRoute/PublicRoute.js

@@ -0,0 +1,18 @@
+import { useSelector } from 'react-redux';
+import { Route, Redirect } from 'react-router-dom';
+import { getToken } from '../../../redux/authorization/selector';
+
+export default function PublicRoute({
+  children,
+  restricted = false,
+  ...routeProps
+}) {
+  const token = useSelector(getToken);
+  const shouldRedirect = token && restricted;
+
+  return (
+    <Route {...routeProps}>
+      {shouldRedirect ? <Redirect to="/" /> : children}
+    </Route>
+  );
+}

+ 43 - 0
src/index.css

@@ -0,0 +1,43 @@
+.app_wrapper {
+  background-color: rgb(119, 110, 110);
+  padding-bottom: 20px;
+  min-width: 100vw;
+  min-height: 100vh;
+}
+
+html {
+  overflow-y: scroll;
+  overflow-x: hidden;
+}
+
+body {
+  min-width: 100vw;
+  min-height: 100vh;
+  padding: 0;
+}
+
+code {
+  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
+    monospace;
+}
+h3,
+h4 {
+  margin: 0;
+}
+
+img {
+  display: block;
+  max-width: 100%;
+  object-fit: cover;
+  border-radius: 5%;
+}
+
+.box.big {
+  width: 200px;
+  height: 200px;
+  margin-bottom: 100px;
+}
+
+.box.red {
+  background-color: teal;
+}

+ 22 - 0
src/index.js

@@ -0,0 +1,22 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import { Provider } from 'react-redux';
+import { PersistGate } from 'redux-persist/integration/react';
+import 'modern-normalize/modern-normalize.css';
+
+import './index.css';
+import App from './App';
+import { store, persistor } from './redux/store';
+
+ReactDOM.render(
+  <React.StrictMode>
+    <div className="app_wrapper">
+      <PersistGate loading={null} persistor={persistor}>
+        <Provider store={store}>
+          <App />
+        </Provider>
+      </PersistGate>
+    </div>
+  </React.StrictMode>,
+  document.getElementById('root'),
+);

+ 24 - 0
src/redux/authorization/action/index.js

@@ -0,0 +1,24 @@
+import { createAction } from '@reduxjs/toolkit';
+
+const actionLogInSuccess = createAction('login/success', value => ({
+  payload: value,
+}));
+
+const actionLogInReject = createAction('login/reject', value => ({
+  payload: value,
+}));
+
+const actionLogOutSuccess = createAction('logout/success', value => ({
+  payload: value,
+}));
+
+const actionLogOutReject = createAction('logout/reject', value => ({
+  payload: value,
+}));
+
+export {
+  actionLogInSuccess,
+  actionLogInReject,
+  actionLogOutSuccess,
+  actionLogOutReject,
+};

+ 52 - 0
src/redux/authorization/operations/index.js

@@ -0,0 +1,52 @@
+import { actionIsLoading } from '../../loading/action';
+
+import {
+  actionLogInSuccess,
+  actionLogInReject,
+  actionLogOutSuccess,
+  actionLogOutReject,
+} from '../action';
+
+import { loginGQL, registerGQL } from '../../../api-data';
+
+const asyncLogin = (login, password) => async dispatch => {
+  try {
+    dispatch(actionIsLoading(true));
+    const token = await loginGQL(login, password);
+    if (!token) throw new Error('Wrong credentials');
+    localStorage.token = token;
+    dispatch(actionLogInSuccess({ login, token }));
+  } catch (e) {
+    dispatch(actionLogInReject());
+  } finally {
+    dispatch(actionIsLoading(false));
+  }
+};
+
+const asyncLogout = () => async dispatch => {
+  try {
+    dispatch(actionIsLoading(true));
+    dispatch(actionLogOutSuccess({ login: '', token: '' }));
+    localStorage.token = null;
+  } catch (e) {
+    dispatch(actionLogOutReject());
+  } finally {
+    dispatch(actionIsLoading(false));
+  }
+};
+
+const asyncCreateUser = (login, password) => async dispatch => {
+  try {
+    dispatch(actionIsLoading(true));
+    await registerGQL(login, password);
+    const token = await loginGQL(login, password);
+    if (!token) throw new Error('Server error');
+    dispatch(actionLogInSuccess({ login, token }));
+  } catch (e) {
+    console.error('Credentials have already used', e);
+  } finally {
+    dispatch(actionIsLoading(false));
+  }
+};
+
+export { asyncLogin, asyncLogout, asyncCreateUser };

+ 27 - 0
src/redux/authorization/reducer/index.js

@@ -0,0 +1,27 @@
+import { createReducer } from '@reduxjs/toolkit';
+
+import {
+  actionLogInSuccess,
+  actionLogInReject,
+  actionLogOutSuccess,
+  actionLogOutReject,
+} from '../action';
+
+const initialState = { login: '', token: '' };
+
+const reducerAuthorization = createReducer(initialState, {
+  [actionLogInSuccess]: (_, { payload }) => {
+    return payload;
+  },
+  [actionLogInReject]: (state, _) => {
+    return state;
+  },
+  [actionLogOutSuccess]: (_, { payload }) => {
+    return payload;
+  },
+  [actionLogOutReject]: (state, _) => {
+    return state;
+  },
+});
+
+export default reducerAuthorization;

+ 4 - 0
src/redux/authorization/selector/index.js

@@ -0,0 +1,4 @@
+const getToken = state => state.authorization.token;
+const getLogin = state => state.authorization.login;
+
+export { getToken, getLogin };

+ 24 - 0
src/redux/categories/action/index.js

@@ -0,0 +1,24 @@
+import { createAction } from '@reduxjs/toolkit';
+
+const actionCategoriesSuccess = createAction('categories/success', value => ({
+  payload: value,
+}));
+
+const actionCategoriesReject = createAction('categories/reject', value => ({
+  payload: value,
+}));
+
+const actionCategorySuccess = createAction('category/success', value => ({
+  payload: value,
+}));
+
+const actionCategoryReject = createAction('category/reject', value => ({
+  payload: value,
+}));
+
+export {
+  actionCategoriesSuccess,
+  actionCategoriesReject,
+  actionCategorySuccess,
+  actionCategoryReject,
+};

+ 36 - 0
src/redux/categories/operations/index.js

@@ -0,0 +1,36 @@
+import { actionIsLoading } from '../../loading/action';
+
+import {
+  actionCategoriesSuccess,
+  actionCategoriesReject,
+  actionCategorySuccess,
+  actionCategoryReject,
+} from '../action';
+
+import { categoriesGQL, categoryById } from '../../../api-data';
+
+const asyncGetCategories = () => async dispatch => {
+  try {
+    dispatch(actionIsLoading(true));
+    const data = await categoriesGQL();
+    dispatch(actionCategoriesSuccess(data));
+  } catch (e) {
+    dispatch(actionCategoriesReject());
+  } finally {
+    dispatch(actionIsLoading(false));
+  }
+};
+
+const asyncGetCategoryById = id => async dispatch => {
+  try {
+    dispatch(actionIsLoading(true));
+    const data = await categoryById(id);
+    dispatch(actionCategorySuccess(data));
+  } catch (e) {
+    dispatch(actionCategoryReject());
+  } finally {
+    dispatch(actionIsLoading(false));
+  }
+};
+
+export { asyncGetCategories, asyncGetCategoryById };

+ 29 - 0
src/redux/categories/reducer/index.js

@@ -0,0 +1,29 @@
+import { createReducer } from '@reduxjs/toolkit';
+
+import {
+  actionCategoriesSuccess,
+  actionCategoriesReject,
+  actionCategorySuccess,
+  actionCategoryReject,
+} from '../action';
+
+const initialState = { categories: [], category: [] };
+
+const reducerCategories = createReducer(initialState, {
+  [actionCategoriesSuccess]: (state, { payload: categories }) => {
+    const category = state.category;
+    return { categories, category };
+  },
+  [actionCategoriesReject]: (state, _) => {
+    return state;
+  },
+  [actionCategorySuccess]: (state, { payload: category }) => {
+    const categories = state.categories;
+    return { categories, category: [category] };
+  },
+  [actionCategoryReject]: (state, _) => {
+    return state;
+  },
+});
+
+export default reducerCategories;

+ 4 - 0
src/redux/categories/selector/index.js

@@ -0,0 +1,4 @@
+const getCategories = state => state.categories.categories;
+const getCategory = state => state.categories.category;
+
+export { getCategories, getCategory };

+ 24 - 0
src/redux/goods/action/index.js

@@ -0,0 +1,24 @@
+import { createAction } from '@reduxjs/toolkit';
+
+const actionGoodsSuccess = createAction('goods/success', value => ({
+  payload: value,
+}));
+
+const actionGoodsReject = createAction('goods/reject', value => ({
+  payload: value,
+}));
+
+const actionGoodSuccess = createAction('good/success', value => ({
+  payload: value,
+}));
+
+const actionGoodReject = createAction('good/reject', value => ({
+  payload: value,
+}));
+
+export {
+  actionGoodsSuccess,
+  actionGoodsReject,
+  actionGoodSuccess,
+  actionGoodReject,
+};

+ 36 - 0
src/redux/goods/operations/index.js

@@ -0,0 +1,36 @@
+import { actionIsLoading } from '../../loading/action';
+
+import {
+  actionGoodsSuccess,
+  actionGoodsReject,
+  actionGoodSuccess,
+  actionGoodReject,
+} from '../action';
+
+import { goodsGQL, goodById } from '../../../api-data';
+
+const asyncGetGoods = () => async dispatch => {
+  try {
+    dispatch(actionIsLoading(true));
+    const data = await goodsGQL();
+    dispatch(actionGoodsSuccess(data));
+  } catch (e) {
+    dispatch(actionGoodsReject());
+  } finally {
+    dispatch(actionIsLoading(false));
+  }
+};
+
+const asyncGetGoodById = id => async dispatch => {
+  try {
+    dispatch(actionIsLoading(true));
+    const data = await goodById(id);
+    dispatch(actionGoodSuccess(data));
+  } catch (e) {
+    dispatch(actionGoodReject());
+  } finally {
+    dispatch(actionIsLoading(false));
+  }
+};
+
+export { asyncGetGoods, asyncGetGoodById };

+ 29 - 0
src/redux/goods/reducer/index.js

@@ -0,0 +1,29 @@
+import { createReducer } from '@reduxjs/toolkit';
+
+import {
+  actionGoodsSuccess,
+  actionGoodsReject,
+  actionGoodSuccess,
+  actionGoodReject,
+} from '../action';
+
+const initialState = { goods: [], good: [] };
+
+const reducerGoods = createReducer(initialState, {
+  [actionGoodsSuccess]: (state, { payload: goods }) => {
+    const good = state.good;
+    return { goods, good };
+  },
+  [actionGoodsReject]: (state, _) => {
+    return state;
+  },
+  [actionGoodSuccess]: (state, { payload: good }) => {
+    const goods = state.goods;
+    return { goods, good: [good] };
+  },
+  [actionGoodReject]: (state, _) => {
+    return state;
+  },
+});
+
+export default reducerGoods;

+ 4 - 0
src/redux/goods/selector/index.js

@@ -0,0 +1,4 @@
+const getGoods = state => state.goods.goods;
+const getGood = state => state.goods.good;
+
+export { getGoods, getGood };

+ 7 - 0
src/redux/loading/action/index.js

@@ -0,0 +1,7 @@
+import { createAction } from '@reduxjs/toolkit';
+
+const actionIsLoading = createAction('fetch/loading', value => ({
+  payload: value,
+}));
+
+export { actionIsLoading };

+ 7 - 0
src/redux/loading/reducer/index.js

@@ -0,0 +1,7 @@
+import { createReducer } from '@reduxjs/toolkit';
+import { actionIsLoading } from '../action';
+
+const reducerLoading = createReducer(false, {
+  [actionIsLoading]: (_, { payload }) => payload,
+});
+export default reducerLoading;

+ 3 - 0
src/redux/loading/selector/index.js

@@ -0,0 +1,3 @@
+const getLoad = state => state.isLoading;
+
+export { getLoad };

+ 24 - 0
src/redux/orders/action/index.js

@@ -0,0 +1,24 @@
+import { createAction } from '@reduxjs/toolkit';
+
+const actionOrdersSuccess = createAction('orders/success', value => ({
+  payload: value,
+}));
+
+const actionOrdersReject = createAction('orders/reject', value => ({
+  payload: value,
+}));
+
+const actionOrderSuccess = createAction('order/success', value => ({
+  payload: value,
+}));
+
+const actionOrderReject = createAction('order/reject', value => ({
+  payload: value,
+}));
+
+export {
+  actionOrdersSuccess,
+  actionOrdersReject,
+  actionOrderSuccess,
+  actionOrderReject,
+};

+ 36 - 0
src/redux/orders/operations/index.js

@@ -0,0 +1,36 @@
+import { actionIsLoading } from '../../loading/action';
+
+import {
+  actionOrdersSuccess,
+  actionOrdersReject,
+  actionOrderSuccess,
+  actionOrderReject,
+} from '../action';
+
+import { ordersGQL, orderById } from '../../../api-data';
+
+const asyncGetOrders = () => async dispatch => {
+  try {
+    dispatch(actionIsLoading(true));
+    const data = await ordersGQL();
+    dispatch(actionOrdersSuccess(data));
+  } catch (e) {
+    dispatch(actionOrdersReject());
+  } finally {
+    dispatch(actionIsLoading(false));
+  }
+};
+
+const asyncGetOrderById = id => async dispatch => {
+  try {
+    dispatch(actionIsLoading(true));
+    const data = await orderById(id);
+    dispatch(actionOrderSuccess(data));
+  } catch (e) {
+    dispatch(actionOrderReject());
+  } finally {
+    dispatch(actionIsLoading(false));
+  }
+};
+
+export { asyncGetOrders, asyncGetOrderById };

+ 29 - 0
src/redux/orders/reducer/index.js

@@ -0,0 +1,29 @@
+import { createReducer } from '@reduxjs/toolkit';
+
+import {
+  actionOrdersSuccess,
+  actionOrdersReject,
+  actionOrderSuccess,
+  actionOrderReject,
+} from '../action';
+
+const initialState = { orders: [], order: [] };
+
+const reducerOrders = createReducer(initialState, {
+  [actionOrdersSuccess]: (state, { payload: orders }) => {
+    const order = state.order;
+    return { orders, order };
+  },
+  [actionOrdersReject]: (state, _) => {
+    return state;
+  },
+  [actionOrderSuccess]: (state, { payload: order }) => {
+    const orders = state.orders;
+    return { orders, order: [order] };
+  },
+  [actionOrderReject]: (state, _) => {
+    return state;
+  },
+});
+
+export default reducerOrders;

+ 4 - 0
src/redux/orders/selector/index.js

@@ -0,0 +1,4 @@
+const getOrders = state => state.orders.orders;
+const getOrder = state => state.orders.order;
+
+export { getOrders, getOrder };

+ 25 - 0
src/redux/rootReducer/index.js

@@ -0,0 +1,25 @@
+import { combineReducers } from '@reduxjs/toolkit';
+import { persistReducer } from 'redux-persist';
+import storage from 'redux-persist/lib/storage';
+
+import reducerCategories from '../categories/reducer';
+import reducerGoods from '../goods/reducer';
+import reducerOrders from '../orders/reducer';
+import reducerLoading from '../loading/reducer';
+import reducerAuthorization from '../authorization/reducer';
+
+const authorizationPersistConfig = {
+  key: 'auth',
+  storage: storage,
+};
+
+export const rootReducer = combineReducers({
+  categories: reducerCategories,
+  goods: reducerGoods,
+  orders: reducerOrders,
+  isLoading: reducerLoading,
+  authorization: persistReducer(
+    authorizationPersistConfig,
+    reducerAuthorization,
+  ),
+});

+ 14 - 0
src/redux/store/index.js

@@ -0,0 +1,14 @@
+import { createStore, applyMiddleware } from 'redux';
+import thunk from 'redux-thunk';
+import { persistStore } from 'redux-persist';
+import { composeWithDevTools } from 'redux-devtools-extension';
+
+import { rootReducer } from '../rootReducer';
+
+const composeEnhancers = composeWithDevTools({});
+const store = createStore(
+  rootReducer,
+  composeEnhancers(applyMiddleware(thunk)),
+);
+const persistor = persistStore(store);
+export { store, persistor };