Pārlūkot izejas kodu

Merge pull request #1 from ilya2033/dev

Dev
Ilya_Shyian 2 gadi atpakaļ
vecāks
revīzija
3b8fd69992
100 mainītis faili ar 8106 papildinājumiem un 2914 dzēšanām
  1. 4125 2895
      package-lock.json
  2. 20 1
      package.json
  3. 19 18
      src/App.js
  4. 25 0
      src/actions/actionCatAll.js
  5. 23 0
      src/actions/actionCatById.js
  6. 7 0
      src/actions/actionCategoryUpdate.js
  7. 28 0
      src/actions/actionCategoryUpsert.js
  8. 24 0
      src/actions/actionCatsFind.js
  9. 23 0
      src/actions/actionGoodById.js
  10. 7 0
      src/actions/actionGoodUpdate.js
  11. 31 0
      src/actions/actionGoodUpsert.js
  12. 25 0
      src/actions/actionGoodsAll.js
  13. 24 0
      src/actions/actionGoodsFind.js
  14. 24 0
      src/actions/actionGoodsPopular.js
  15. 32 0
      src/actions/actionLogin.js
  16. 7 0
      src/actions/actionLogout.js
  17. 23 0
      src/actions/actionOrderById.js
  18. 8 0
      src/actions/actionOrderUpdate.js
  19. 33 0
      src/actions/actionOrderUpsert.js
  20. 31 0
      src/actions/actionOrdersAll.js
  21. 24 0
      src/actions/actionOrdersFind.js
  22. 9 0
      src/actions/actionPageStart.js
  23. 73 0
      src/actions/actionRootCats.js
  24. 20 0
      src/actions/actionUploadFile.js
  25. 8 0
      src/actions/actionUploadFiles.js
  26. 126 0
      src/components/AuthPage/AuthForm.js
  27. 13 0
      src/components/AuthPage/index.js
  28. 82 0
      src/components/CartPage/CartItem.js
  29. 192 0
      src/components/CartPage/OrderForm/index.js
  30. 65 0
      src/components/CartPage/index.js
  31. 44 0
      src/components/GoodPage/index.js
  32. 12 0
      src/components/GoodsPage/SubCategories/index.js
  33. 53 0
      src/components/GoodsPage/index.js
  34. 87 0
      src/components/LayoutPage/index.js
  35. 27 0
      src/components/MainPage/index.js
  36. 36 0
      src/components/Root/index.js
  37. 48 0
      src/components/UIContext/index.js
  38. 17 0
      src/components/admin/AdminCategoriesPage/AdminCategoryItem.js
  39. 57 0
      src/components/admin/AdminCategoriesPage/AdminCategoryList.js
  40. 50 0
      src/components/admin/AdminCategoriesPage/AdminCategoryListHeader.js
  41. 13 0
      src/components/admin/AdminCategoriesPage/index.js
  42. 182 0
      src/components/admin/AdminCategoryPage/CategoryForm.js
  43. 12 0
      src/components/admin/AdminCategoryPage/index.js
  44. 221 0
      src/components/admin/AdminGoodPage/GoodForm.js
  45. 12 0
      src/components/admin/AdminGoodPage/index.js
  46. 35 0
      src/components/admin/AdminGoodsPage/AdminGoodItem.js
  47. 58 0
      src/components/admin/AdminGoodsPage/AdminGoodList.js
  48. 61 0
      src/components/admin/AdminGoodsPage/AdminGoodListHeader.js
  49. 13 0
      src/components/admin/AdminGoodsPage/index.js
  50. 216 0
      src/components/admin/AdminLayoutPage/index.js
  51. 272 0
      src/components/admin/AdminOrderPage/OrderForm.js
  52. 31 0
      src/components/admin/AdminOrderPage/OrderGoodsEditor/OrderGood.js
  53. 68 0
      src/components/admin/AdminOrderPage/OrderGoodsEditor/index.js
  54. 12 0
      src/components/admin/AdminOrderPage/index.js
  55. 30 0
      src/components/admin/AdminOrdersPage/AdminOrderItem.js
  56. 52 0
      src/components/admin/AdminOrdersPage/AdminOrderList.js
  57. 52 0
      src/components/admin/AdminOrdersPage/AdminOrderListHeader.js
  58. 13 0
      src/components/admin/AdminOrdersPage/index.js
  59. 8 0
      src/components/common/AddButton/index.js
  60. 36 0
      src/components/common/BuyButton/index.js
  61. 14 0
      src/components/common/Categories/Category.js
  62. 19 0
      src/components/common/Categories/index.js
  63. 66 0
      src/components/common/DrawerCart/DrawerCart.js
  64. 111 0
      src/components/common/DrawerCart/DrawerCartItem.js
  65. 11 0
      src/components/common/DrawerRight/index.js
  66. 20 0
      src/components/common/DropZone/index.js
  67. 2 0
      src/components/common/EntityEditor/SortableItem.js
  68. 5 0
      src/components/common/EntityEditor/SortableList.js
  69. 81 0
      src/components/common/EntityEditor/index.js
  70. 9 0
      src/components/common/Error/index.js
  71. 4 0
      src/components/common/Error404/index.js
  72. 37 0
      src/components/common/GoodCard/index.js
  73. 14 0
      src/components/common/GoodCardSlider/GoodCardSet.js
  74. 37 0
      src/components/common/GoodCardSlider/GoodCardSlider.js
  75. 3 0
      src/components/common/GoodCardSlider/index.js
  76. 16 0
      src/components/common/GoodList/index.js
  77. 14 0
      src/components/common/ProtectedRoute/index.js
  78. 109 0
      src/components/common/SearchBar/SearchBar.js
  79. 17 0
      src/components/common/SearchBar/SearchCategoryResultItem.js
  80. 37 0
      src/components/common/SearchBar/SearchGoodResultItem.js
  81. 23 0
      src/components/common/SearchBar/SearchOrderResultItem.js
  82. 71 0
      src/components/common/SearchBar/SearchResults.js
  83. 7 0
      src/components/common/SearchBar/index.js
  84. 60 0
      src/components/common/SortOptions/index.js
  85. 18 0
      src/components/layout/Aside/AdminCategories.js
  86. 8 0
      src/components/layout/Aside/CCategories.js
  87. 28 0
      src/components/layout/Aside/StatusOptions.js
  88. 46 0
      src/components/layout/Aside/index.js
  89. 5 0
      src/components/layout/Content/index.js
  90. 94 0
      src/components/layout/Footer/index.js
  91. 24 0
      src/components/layout/Header/CartIcon/index.js
  92. 19 0
      src/components/layout/Header/LogoutIcon/index.js
  93. 56 0
      src/components/layout/Header/index.js
  94. 1 0
      src/helpers/delay.js
  95. 1 0
      src/helpers/getQuery.js
  96. 6 0
      src/helpers/index.js
  97. 9 0
      src/helpers/jwtDecode.js
  98. 0 0
      src/helpers/mock.js
  99. 25 0
      src/helpers/orderStatus.js
  100. 0 0
      src/helpers/sortOptions.js

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 4125 - 2895
package-lock.json


+ 20 - 1
package.json

@@ -1,15 +1,34 @@
 {
   "name": "diploma",
   "version": "0.1.0",
+  "proxy":"https://diploma-back-django.herokuapp.com/",
   "private": true,
   "dependencies": {
+    "@dnd-kit/core": "^5.0.3",
+    "@emotion/react": "^11.9.0",
+    "@emotion/styled": "^11.8.1",
+    "@mui/material": "^5.6.4",
     "@testing-library/jest-dom": "^5.16.4",
     "@testing-library/react": "^13.2.0",
     "@testing-library/user-event": "^13.5.0",
+    "array-move": "^4.0.0",
+    "formik": "^2.2.9",
+    "node-sass": "^7.0.1",
     "react": "^18.1.0",
     "react-dom": "^18.1.0",
+    "react-dropzone": "^14.2.1",
+    "react-icons": "^4.3.1",
+    "react-redux": "^8.0.1",
+    "react-responsive-carousel": "^3.2.23",
+    "react-router-dom": "^6.3.0",
     "react-scripts": "5.0.1",
-    "web-vitals": "^2.1.4"
+    "react-select": "^5.3.2",
+    "react-sortable-hoc": "^2.0.0",
+    "redux": "^4.2.0",
+    "redux-devtools-extension": "^2.13.9",
+    "redux-thunk": "^2.4.1",
+    "web-vitals": "^2.1.4",
+    "yup": "^0.32.11"
   },
   "scripts": {
     "start": "react-scripts start",

+ 19 - 18
src/App.js

@@ -1,25 +1,26 @@
 import logo from './logo.svg';
 import './App.css';
+import { Box } from '@mui/material';
+import { BrowserRouter } from 'react-router-dom';
+import { Router } from 'react-router-dom';
+import { Root } from './components/Root';
+import { Provider } from 'react-redux';
+import { store } from './reducers';
+import { UIContextProvider } from './components/UIContext';
 
 function App() {
-  return (
-    <div className="App">
-      <header className="App-header">
-        <img src={logo} className="App-logo" alt="logo" />
-        <p>
-          Edit <code>src/App.js</code> and save to reload.
-        </p>
-        <a
-          className="App-link"
-          href="https://reactjs.org"
-          target="_blank"
-          rel="noopener noreferrer"
-        >
-          Learn React
-        </a>
-      </header>
-    </div>
-  );
+    return (
+        <Provider store={store}>
+            <BrowserRouter>
+                <UIContextProvider>
+                    <Box className="App">
+                        <Root />
+                    </Box>
+                </UIContextProvider>
+            </BrowserRouter>
+        </Provider>
+    );
 }
 
+store.subscribe(() => console.log(store.getState()));
 export default App;

+ 25 - 0
src/actions/actionCatAll.js

@@ -0,0 +1,25 @@
+import { actionPromise } from '../reducers';
+import { gql } from '../helpers';
+
+export const actionCatAll =
+    ({ limit = 20, skip = 0, promiseName = 'catAll', orderBy = '' } = {}) =>
+    async (dispatch, getState) => {
+        dispatch(
+            actionPromise(
+                promiseName,
+                fetch(`/categories/?limit=${limit}&skip=${skip}${orderBy && `&orderBy=` + orderBy}`, {
+                    method: 'GET',
+                    headers: {
+                        'Content-Type': 'application/json',
+                        ...(localStorage.authToken ? { Authorization: 'Bearer ' + localStorage.authToken } : {}),
+                    },
+                })
+                    .then((res) => res.json())
+                    .then((data) => {
+                        if (data.errors) {
+                            throw new Error(JSON.stringify(data.errors));
+                        } else return data.data;
+                    })
+            )
+        );
+    };

+ 23 - 0
src/actions/actionCatById.js

@@ -0,0 +1,23 @@
+import { mock, query } from '../helpers';
+
+import { actionPromise } from '../reducers';
+
+export const actionCatById = ({ _id, promiseName = 'catById', orderBy = '', limit = 20, skip = 0 }) =>
+    actionPromise(
+        promiseName,
+        fetch(`/categories/${_id}/?limit=${limit}&skip=${skip}${orderBy && `&orderBy=` + orderBy}`, {
+            method: 'GET',
+            headers: {
+                'Content-Type': 'application/json',
+                Accept: 'application/json',
+
+                ...(localStorage.authToken ? { Authorization: 'Bearer ' + localStorage.authToken } : {}),
+            },
+        })
+            .then((res) => res.json())
+            .then((data) => {
+                if (data.errors) {
+                    throw new Error(JSON.stringify(data.errors));
+                } else return data.data;
+            })
+    );

+ 7 - 0
src/actions/actionCategoryUpdate.js

@@ -0,0 +1,7 @@
+import { actionCatAll } from './actionCatAll';
+import { actionCategoryUpsert } from './actionCategoryUpsert';
+
+export const actionCategoryUpdate = (good) => async (dispatch, getState) => {
+    await dispatch(actionCategoryUpsert(good));
+    await dispatch(actionCatAll());
+};

+ 28 - 0
src/actions/actionCategoryUpsert.js

@@ -0,0 +1,28 @@
+import { actionPromise } from '../reducers';
+
+export const actionCategoryUpsert = (category) => async (dispatch) => {
+    const formData = new FormData();
+    category._id && formData.append('_id', category._id);
+    formData.append('name', category.name);
+    formData.append('goods', JSON.stringify(category.goods));
+    category.parent && formData.append('parent', JSON.stringify(category.parent));
+    formData.append('subcategories', JSON.stringify(category.subcategories));
+    dispatch(
+        actionPromise(
+            'categoryUpsert',
+            fetch(`/category/`, {
+                method: 'POST',
+                headers: {
+                    ...(localStorage.authToken ? { Authorization: 'Bearer ' + localStorage.authToken } : {}),
+                },
+                body: formData,
+            })
+                .then((res) => res.json())
+                .then((data) => {
+                    if (data.errors) {
+                        throw new Error(JSON.stringify(data.errors));
+                    } else return data.data;
+                })
+        )
+    );
+};

+ 24 - 0
src/actions/actionCatsFind.js

@@ -0,0 +1,24 @@
+import { actionPromise } from '../reducers';
+
+export const actionCatsFind =
+    ({ text = '', limit = 7, skip = 0, promiseName = 'catsFind', orderBy = '' }) =>
+    async (dispatch, getState) => {
+        dispatch(
+            actionPromise(
+                promiseName,
+                fetch(`/categories/?limit=${limit}&skip=${skip}&text=${text}${orderBy && `&orderBy=` + orderBy}`, {
+                    method: 'GET',
+                    headers: {
+                        'Content-Type': 'application/json',
+                        ...(localStorage.authToken ? { Authorization: 'Bearer ' + localStorage.authToken } : {}),
+                    },
+                })
+                    .then((res) => res.json())
+                    .then((data) => {
+                        if (data.errors) {
+                            throw new Error(JSON.stringify(data.errors));
+                        } else return data.data;
+                    })
+            )
+        );
+    };

+ 23 - 0
src/actions/actionGoodById.js

@@ -0,0 +1,23 @@
+import { getQuery, mock, query } from '../helpers';
+
+import { actionPromise } from '../reducers';
+
+export const actionGoodById = ({ _id, promiseName = 'goodById' } = {}) =>
+    actionPromise(
+        promiseName,
+        fetch(`/goods/${_id}/`, {
+            method: 'GET',
+            headers: {
+                'Content-Type': 'application/json',
+                Accept: 'application/json',
+
+                ...(localStorage.authToken ? { Authorization: 'Bearer ' + localStorage.authToken } : {}),
+            },
+        })
+            .then((res) => res.json())
+            .then((data) => {
+                if (data.errors) {
+                    throw new Error(JSON.stringify(data.errors));
+                } else return data.data;
+            })
+    );

+ 7 - 0
src/actions/actionGoodUpdate.js

@@ -0,0 +1,7 @@
+import { actionGoodsAll } from './actionGoodsAll';
+import { actionGoodUpsert } from './actionGoodUpsert';
+
+export const actionGoodUpdate = (good) => async (dispatch, getState) => {
+    await dispatch(actionGoodUpsert(good));
+    await dispatch(actionGoodsAll());
+};

+ 31 - 0
src/actions/actionGoodUpsert.js

@@ -0,0 +1,31 @@
+import { actionPromise } from '../reducers';
+
+export const actionGoodUpsert = (good) => async (dispatch) => {
+    const formData = new FormData();
+    console.log(JSON.stringify(good.images));
+    good._id && formData.append('_id', good._id);
+    formData.append('name', good.name);
+    formData.append('description', good.description);
+    formData.append('amount', good.amount);
+    formData.append('price', good.price);
+    formData.append('images', JSON.stringify(good.images));
+    formData.append('categories', JSON.stringify(good.categories));
+    dispatch(
+        actionPromise(
+            'goodUpsert',
+            fetch(`/good/`, {
+                method: 'POST',
+                headers: {
+                    ...(localStorage.authToken ? { Authorization: 'Bearer ' + localStorage.authToken } : {}),
+                },
+                body: formData,
+            })
+                .then((res) => res.json())
+                .then((data) => {
+                    if (data.errors) {
+                        throw new Error(JSON.stringify(data.errors));
+                    } else return data.data;
+                })
+        )
+    );
+};

+ 25 - 0
src/actions/actionGoodsAll.js

@@ -0,0 +1,25 @@
+import { getQuery } from '../helpers';
+import { actionPromise } from '../reducers';
+
+export const actionGoodsAll =
+    ({ limit = 20, skip = 0, promiseName = 'goodsAll', orderBy = '' } = {}) =>
+    async (dispatch, getState) => {
+        dispatch(
+            actionPromise(
+                promiseName,
+                fetch(`/goods/?limit=${limit}&skip=${skip}${orderBy && `&orderBy=` + orderBy}`, {
+                    method: 'GET',
+                    headers: {
+                        'Content-Type': 'application/json',
+                        ...(localStorage.authToken ? { Authorization: 'Bearer ' + localStorage.authToken } : {}),
+                    },
+                })
+                    .then((res) => res.json())
+                    .then((data) => {
+                        if (data.errors) {
+                            throw new Error(JSON.stringify(data.errors));
+                        } else return data.data;
+                    })
+            )
+        );
+    };

+ 24 - 0
src/actions/actionGoodsFind.js

@@ -0,0 +1,24 @@
+import { actionPromise } from '../reducers';
+
+export const actionGoodsFind =
+    ({ text = '', limit = 7, skip = 0, promiseName = 'goodsFind', orderBy = '' }) =>
+    async (dispatch, getState) => {
+        dispatch(
+            actionPromise(
+                promiseName,
+                fetch(`/goods/?limit=${limit}&skip=${skip}&text=${text}${orderBy && `&orderBy=` + orderBy}`, {
+                    method: 'GET',
+                    headers: {
+                        'Content-Type': 'application/json',
+                        ...(localStorage.authToken ? { Authorization: 'Bearer ' + localStorage.authToken } : {}),
+                    },
+                })
+                    .then((res) => res.json())
+                    .then((data) => {
+                        if (data.errors) {
+                            throw new Error(JSON.stringify(data.errors));
+                        } else return data.data;
+                    })
+            )
+        );
+    };

+ 24 - 0
src/actions/actionGoodsPopular.js

@@ -0,0 +1,24 @@
+import { mock, query } from '../helpers';
+
+import { actionPromise } from '../reducers';
+
+export const actionGoodsPopular = () => async (dispatch, getState) => {
+    dispatch(
+        actionPromise(
+            'goodsPopular',
+            fetch(`/goods/?limit=20&skip=0&popular=1`, {
+                method: 'GET',
+                headers: {
+                    'Content-Type': 'application/json',
+                    ...(localStorage.authToken ? { Authorization: 'Bearer ' + localStorage.authToken } : {}),
+                },
+            })
+                .then((res) => res.json())
+                .then((data) => {
+                    if (data.errors) {
+                        throw new Error(JSON.stringify(data.errors));
+                    } else return data.data;
+                })
+        )
+    );
+};

+ 32 - 0
src/actions/actionLogin.js

@@ -0,0 +1,32 @@
+import { actionPromise } from '../reducers';
+import { gql } from '../helpers';
+import { actionAuthLogin } from '../reducers';
+
+export const actionLogin = (login, password) => async (dispatch, getState) => {
+    const formData = new FormData();
+    console.log(login, password);
+    formData.append('username', login);
+    formData.append('password', password);
+
+    const token = await dispatch(
+        actionPromise(
+            'login',
+            fetch(`/auth/token/`, {
+                method: 'POST',
+                headers: {
+                    ...(localStorage.authToken ? { Authorization: 'Bearer ' + localStorage.authToken } : {}),
+                },
+                body: formData,
+            })
+                .then((res) => res.json())
+                .then((data) => {
+                    console.log(data);
+                    if (data.errors) {
+                        throw new Error(JSON.stringify(data.errors));
+                    } else return data.access;
+                })
+        )
+    );
+
+    dispatch(actionAuthLogin(token));
+};

+ 7 - 0
src/actions/actionLogout.js

@@ -0,0 +1,7 @@
+import { actionCartClear, actionPromiseClear } from '../reducers';
+import { actionAuthLogout } from '../reducers';
+
+export const actionLogout = () => async (dispatch) => {
+    dispatch(actionCartClear());
+    dispatch(actionAuthLogout());
+};

+ 23 - 0
src/actions/actionOrderById.js

@@ -0,0 +1,23 @@
+import { mock, query } from '../helpers';
+
+import { actionPromise } from '../reducers';
+
+export const actionOrderById = ({ _id, promiseName = 'orderById' }) =>
+    actionPromise(
+        promiseName,
+        fetch(`/orders/${_id}/`, {
+            method: 'GET',
+            headers: {
+                'Content-Type': 'application/json',
+                Accept: 'application/json',
+
+                ...(localStorage.authToken ? { Authorization: 'Bearer ' + localStorage.authToken } : {}),
+            },
+        })
+            .then((res) => res.json())
+            .then((data) => {
+                if (data.errors) {
+                    throw new Error(JSON.stringify(data.errors));
+                } else return data.data;
+            })
+    );

+ 8 - 0
src/actions/actionOrderUpdate.js

@@ -0,0 +1,8 @@
+import { actionCartClear, actionPromiseClear } from '../reducers';
+import { actionOrdersAll } from './actionOrdersAll';
+import { actionOrderUpsert } from './actionOrderUpsert';
+
+export const actionOrderUpdate = (order) => async (dispatch, getState) => {
+    await dispatch(actionOrderUpsert(order));
+    await dispatch(actionOrdersAll());
+};

+ 33 - 0
src/actions/actionOrderUpsert.js

@@ -0,0 +1,33 @@
+import { actionPromise } from '../reducers';
+
+export const actionOrderUpsert = (order) => async (dispatch) => {
+    const formData = new FormData();
+    order._id && formData.append('_id', order._id);
+    formData.append('orderGoods', JSON.stringify(order.orderGoods || []));
+    formData.append('email', order.email);
+    formData.append('phoneNumber', order.phoneNumber);
+    formData.append('address', order.address);
+    formData.append('delivery', order.delivery);
+    formData.append('name', order.name);
+    formData.append('surname', order.surname);
+    formData.append('status', order.status);
+
+    dispatch(
+        actionPromise(
+            'orderUpsert',
+            fetch(`/order/`, {
+                method: 'POST',
+                headers: {
+                    ...(localStorage.authToken ? { Authorization: 'Bearer ' + localStorage.authToken } : {}),
+                },
+                body: formData,
+            })
+                .then((res) => res.json())
+                .then((data) => {
+                    if (data.errors) {
+                        throw new Error(JSON.stringify(data.errors));
+                    } else return data.data;
+                })
+        )
+    );
+};

+ 31 - 0
src/actions/actionOrdersAll.js

@@ -0,0 +1,31 @@
+import { actionPromise } from '../reducers';
+import { gql } from '../helpers';
+
+export const actionOrdersAll =
+    ({ limit = 0, skip = 0, promiseName = 'adminOrdersAll', orderBy = '', status = 0 } = {}) =>
+    async (dispatch, getState) => {
+        console.log(status);
+        dispatch(
+            actionPromise(
+                promiseName,
+                fetch(
+                    `/orders/?limit=${limit}&skip=${skip}${orderBy && `&orderBy=` + orderBy}${
+                        status ? `&status=` + status : ''
+                    }`,
+                    {
+                        method: 'GET',
+                        headers: {
+                            'Content-Type': 'application/json',
+                            ...(localStorage.authToken ? { Authorization: 'Bearer ' + localStorage.authToken } : {}),
+                        },
+                    }
+                )
+                    .then((res) => res.json())
+                    .then((data) => {
+                        if (data.errors) {
+                            throw new Error(JSON.stringify(data.errors));
+                        } else return data.data;
+                    })
+            )
+        );
+    };

+ 24 - 0
src/actions/actionOrdersFind.js

@@ -0,0 +1,24 @@
+import { actionPromise } from '../reducers';
+
+export const actionOrdersFind =
+    ({ text = '', limit = 7, skip = 0, promiseName = 'ordersFind', orderBy = '' }) =>
+    async (dispatch, getState) => {
+        dispatch(
+            actionPromise(
+                promiseName,
+                fetch(`/orders/?limit=${limit}&skip=${skip}&text=${text}${orderBy && `&orderBy=` + orderBy}`, {
+                    method: 'GET',
+                    headers: {
+                        'Content-Type': 'application/json',
+                        ...(localStorage.authToken ? { Authorization: 'Bearer ' + localStorage.authToken } : {}),
+                    },
+                })
+                    .then((res) => res.json())
+                    .then((data) => {
+                        if (data.errors) {
+                            throw new Error(JSON.stringify(data.errors));
+                        } else return data.data;
+                    })
+            )
+        );
+    };

+ 9 - 0
src/actions/actionPageStart.js

@@ -0,0 +1,9 @@
+import { actionCatAll } from './actionCatAll';
+import { actionGoodsPopular } from './actionGoodsPopular';
+import { actionRootCats } from './actionRootCats';
+
+export const actionPageStart = () => async (dispatch, getState) => {
+    dispatch(actionRootCats());
+    dispatch(actionCatAll());
+    dispatch(actionGoodsPopular());
+};

+ 73 - 0
src/actions/actionRootCats.js

@@ -0,0 +1,73 @@
+import { mock, query } from '../helpers';
+
+import { actionPromise } from '../reducers';
+
+export const actionRootCats = () => async (dispatch, getState) => {
+    dispatch(
+        actionPromise(
+            'rootCats',
+            fetch(`/categories/?isRoot=1`, {
+                method: 'GET',
+                headers: {
+                    'Content-Type': 'application/json',
+                    ...(localStorage.authToken ? { Authorization: 'Bearer ' + localStorage.authToken } : {}),
+                },
+            })
+                .then((res) => res.json())
+                .then((data) => {
+                    if (data.errors) {
+                        throw new Error(JSON.stringify(data.errors));
+                    } else return data.data;
+                })
+        )
+    );
+};
+
+// () => ({
+//     data: [
+//         {
+//             id: 1,
+//             name: 'Categoty 1',
+//         },
+//         {
+//             id: 2,
+//             name: 'Categoty 2',
+//         },
+//         {
+//             id: 3,
+//             name: 'Categoty 3',
+//         },
+//         {
+//             id: 4,
+//             name: 'Categoty 4',
+//         },
+//     ],
+// }),
+
+// .then((data) => {
+//     if (data.errors) {
+//         throw new Error(JSON.stringify(data.errors));
+//     } else return Object.values(data.data)[0];
+// })
+// fetch(backendURL + "/api/reception/patient/", {
+//     method: "GET",
+//     headers: {
+//         "Content-Type": "application/json",
+//         ...(localStorage.authToken ? { Authorization: "Bearer " + localStorage.authToken } : {}),
+//     },
+// });
+
+// actionPromise("patientAll",    fetch(url, {
+//     type:"GET",
+//     headers: {
+//         "Content-Type": "application/json",
+//         ...(localStorage.authToken ? { Authorization: "Bearer " + localStorage.authToken } : {}),
+//     },
+//     body: data,
+// })
+//     .then((res) => res.json())
+//     .then((data) => {
+//         if (typeof data === "string") {
+//             throw new Error(data);
+//         } else return Object.values(data);
+//     }))

+ 20 - 0
src/actions/actionUploadFile.js

@@ -0,0 +1,20 @@
+import { actionPromise } from '../reducers';
+
+export const actionUploadFile = (file) => {
+    const fd = new FormData();
+    fd.append('photo', file);
+    return actionPromise(
+        'uploadFile',
+        fetch('/upload/', {
+            method: 'POST',
+            headers: localStorage.authToken ? { Authorization: 'Bearer ' + localStorage.authToken } : {},
+            body: fd,
+        })
+            .then((res) => res.json())
+            .then((data) => {
+                if (data.errors) {
+                    throw new Error(JSON.stringify(data.errors));
+                } else return data.data;
+            })
+    );
+};

+ 8 - 0
src/actions/actionUploadFiles.js

@@ -0,0 +1,8 @@
+import { actionUploadFile } from './actionUploadFile';
+import { actionPromise } from '../reducers';
+
+export const actionUploadFiles =
+    (files = []) =>
+    async (dispatch, getState) => {
+        dispatch(actionPromise('uploadFiles', Promise.all(files?.map((file) => dispatch(actionUploadFile(file))))));
+    };

+ 126 - 0
src/components/AuthPage/AuthForm.js

@@ -0,0 +1,126 @@
+import { actionLogin } from '../../actions/actionLogin';
+
+import { useState, useEffect, useContext } from 'react';
+import { connect, useSelector } from 'react-redux';
+import { MdVisibility, MdVisibilityOff } from 'react-icons/md';
+import { Box, Button, IconButton, TextField, Stack } from '@mui/material';
+import { useFormik } from 'formik';
+import * as Yup from 'yup';
+import { UIContext } from '../UIContext';
+import { Navigate, useNavigate } from 'react-router-dom';
+
+const signInSchema = Yup.object().shape({
+    username: Yup.string().required("Обов'язкове"),
+    password: Yup.string().required("Обов'язкове"),
+});
+
+export const AuthForm = ({ onSubmit = null, promiseStatus, serverErrors = [] } = {}) => {
+    const [showPassword, setShowPassword] = useState(false);
+    const { setAlert } = useContext(UIContext);
+    const navigate = useNavigate();
+    const token = useSelector((state) => state.auth?.token || null);
+
+    if (token) {
+        navigate('/admin');
+    }
+
+    const formik = useFormik({
+        initialValues: {
+            username: '',
+            password: '',
+        },
+        validationSchema: signInSchema,
+        validateOnChange: true,
+        onSubmit: () => {
+            console.log(formik.values.username, formik.values.password);
+            onSubmit(formik.values.username, formik.values.password);
+        },
+    });
+
+    useEffect(() => {
+        if (promiseStatus === 'FULFILLED') {
+            formik.setSubmitting(false);
+            setAlert({
+                show: true,
+                severity: 'success',
+                message: 'Готово',
+            });
+        }
+        if (promiseStatus === 'REJECTED') {
+            const errorMessage = serverErrors.reduce((prev, curr) => prev + '\n' + curr.message, '');
+            formik.setSubmitting(false);
+            setAlert({
+                show: true,
+                severity: 'error',
+                message: errorMessage,
+            });
+        }
+    }, [promiseStatus]);
+
+    return (
+        <Box
+            className="AuthForm"
+            display="flex"
+            flexDirection="column"
+            onSubmit={formik.handleSubmit}
+            justifyContent="center"
+            component="form"
+        >
+            <TextField
+                id="username"
+                name="username"
+                variant="outlined"
+                label="Username"
+                error={formik.touched.username && Boolean(formik.errors.username)}
+                value={formik.values.username}
+                onBlur={formik.handleBlur}
+                onChange={formik.handleChange}
+                helperText={formik.touched.username && formik.errors.username}
+                fullWidth
+                sx={{ mt: 2 }}
+            />
+
+            <TextField
+                id="password"
+                name="password"
+                variant="outlined"
+                label="Password"
+                type={showPassword ? 'text' : 'password'}
+                error={formik.touched.password && Boolean(formik.errors.password)}
+                value={formik.values.password}
+                onBlur={formik.handleBlur}
+                onChange={formik.handleChange}
+                helperText={formik.touched.password && formik.errors.password}
+                InputProps={{
+                    endAdornment: (
+                        <IconButton onClick={() => setShowPassword((prev) => !prev)} edge="end">
+                            {showPassword ? <MdVisibilityOff /> : <MdVisibility />}
+                        </IconButton>
+                    ),
+                }}
+                fullWidth
+                sx={{ mt: 2 }}
+            />
+            <Button
+                variant="contained"
+                color="primary"
+                type="submit"
+                disabled={formik.isSubmitting || !formik.isValid}
+                sx={{ mt: 2, mr: 1 }}
+                fullWidth
+            >
+                Увійти
+            </Button>
+        </Box>
+    );
+};
+
+export const CAuthForm = connect(
+    (state) => ({
+        promiseStatus: state.promise?.login?.status || null,
+        serverErrors: state.promise?.login?.error || [],
+    }),
+    {
+        onSubmit: (login, password) => actionLogin(login, password),
+    }
+)(AuthForm);

+ 13 - 0
src/components/AuthPage/index.js

@@ -0,0 +1,13 @@
+import { Box, Container } from '@mui/material';
+import { useEffect } from 'react';
+import { useSelector } from 'react-redux';
+import { useNavigate } from 'react-router-dom';
+import { CAuthForm } from './AuthForm';
+
+export const AuthPage = () => {
+    return (
+        <Box className="AuthPage">
+            <CAuthForm />
+        </Box>
+    );
+};

+ 82 - 0
src/components/CartPage/CartItem.js

@@ -0,0 +1,82 @@
+import { Box, width } from '@mui/system';
+import defaultGoodImage from '../../images/default-good-image.png';
+import { IoCloseOutline } from 'react-icons/io5';
+import { AiOutlinePlus, AiOutlineMinus } from 'react-icons/ai';
+import { actionCartChange } from '../../reducers';
+import { useEffect, useState } from 'react';
+import { useDispatch } from 'react-redux';
+
+const {
+    Typography,
+    Stack,
+    IconButton,
+    TextField,
+    ButtonGroup,
+    Button,
+    TableCell,
+    TableRow,
+    Input,
+} = require('@mui/material');
+
+export const CartItem = ({ order, onDeleteClick }) => {
+    const { good, count = 1 } = order || {};
+    const { _id, images = [], name = '', price = 0, amount = 1 } = good;
+
+    const dispatch = useDispatch();
+    const [countInput, setCountInput] = useState(count || 1);
+
+    useEffect(() => {
+        setCountInput(+count);
+    }, [count]);
+
+    useEffect(() => {
+        dispatch(actionCartChange(good, +countInput));
+    }, [countInput]);
+
+    const handleChange = (count) => {
+        if (count >= 0 && count <= 99 && count <= amount) {
+            setCountInput(+count);
+        }
+    };
+
+    return (
+        <TableRow className="CartItem">
+            <TableCell>
+                <Box
+                    component="img"
+                    src={images && images[0]?.url ? `${images ? images[0]?.url : ''}` : defaultGoodImage}
+                    sx={{ width: 50 }}
+                />
+            </TableCell>
+            <TableCell>
+                <Box sx={{ flexGrow: 1 }}>
+                    <Typography variant="h5">{name}</Typography>
+                    <Typography variant="body1">{price}</Typography>
+                </Box>
+            </TableCell>
+            <TableCell>
+                <Stack justifyContent="center" direction="row" alignItems="center">
+                    <IconButton onClick={() => handleChange(countInput - 1)}>
+                        <AiOutlineMinus />
+                    </IconButton>
+                    <Typography>{countInput}</Typography>
+                    <IconButton onClick={() => handleChange(countInput + 1)}>
+                        <AiOutlinePlus />
+                    </IconButton>
+                </Stack>
+            </TableCell>
+            <TableCell>
+                <Stack justifyContent="center">
+                    <Typography variant="body1" textAlign="center">
+                        {price * count}
+                    </Typography>
+                </Stack>
+            </TableCell>
+            <TableCell>
+                <IconButton onClick={() => onDeleteClick({ _id, images, name, price })}>
+                    <IoCloseOutline />
+                </IconButton>
+            </TableCell>
+        </TableRow>
+    );
+};

+ 192 - 0
src/components/CartPage/OrderForm/index.js

@@ -0,0 +1,192 @@
+import { Box, Grid, TextField, MenuItem, Button, Alert, Snackbar } from '@mui/material';
+import * as Yup from 'yup';
+import { useFormik } from 'formik';
+import { connect, useDispatch } from 'react-redux';
+import { actionCartClear, actionPromiseClear } from '../../../reducers';
+import { actionOrderUpdate } from '../../../actions/actionOrderUpdate';
+import { useContext, useEffect, useState } from 'react';
+import { UIContext } from '../../UIContext';
+
+const deliveryOptions = [
+    { label: 'Нова пошта', value: 'nova-poshta' },
+    { label: 'Justin', value: 'justin' },
+];
+
+const phoneRegExp =
+    /^((\\+[1-9]{1,4}[ \\-]*)|(\\([0-9]{2,3}\\)[ \\-]*)|([0-9]{2,4})[ \\-]*)*?[0-9]{3,4}?[ \\-]*[0-9]{3,4}?$/;
+const orderSchema = Yup.object().shape({
+    name: Yup.string().min(3, 'не меньше 3 символів').max(22, 'не більше 22 символів').required("обов'язкове"),
+    surname: Yup.string().min(3, 'не меньше 3 символів').max(22, 'не більше 22 символів').required("обов'язкове"),
+    email: Yup.string().email('не вірний формат').required("обов'язкове"),
+    address: Yup.string().required("обов'язкове"),
+    phoneNumber: Yup.string().matches(phoneRegExp, 'не вірний формат').required("обов'язкове"),
+    delivery: Yup.string()
+        .required("обов'язкове")
+        .oneOf(
+            deliveryOptions.map((option) => option.value),
+            'не знайдено'
+        ),
+});
+
+export const OrderForm = ({ onSubmit = null, promiseStatus = null, serverErrors = [] } = {}) => {
+    const { setAlert } = useContext(UIContext);
+    const formik = useFormik({
+        initialValues: {
+            name: '',
+            surname: '',
+            email: '',
+            address: '',
+            phoneNumber: '',
+            delivery: '',
+        },
+        validationSchema: orderSchema,
+        validateOnChange: true,
+        onSubmit: () => {
+            onSubmit(formik.values);
+        },
+    });
+    const dispatch = useDispatch();
+
+    useEffect(() => {
+        if (promiseStatus === 'FULFILLED') {
+            formik.setSubmitting(false);
+            setAlert({
+                show: true,
+                severity: 'success',
+                message: 'Готово',
+            });
+            dispatch(actionPromiseClear('orderUpsert'));
+            dispatch(actionCartClear());
+        }
+        if (promiseStatus === 'REJECTED') {
+            const errorMessage = serverErrors.reduce((prev, curr) => prev + '\n' + curr.message, '');
+            setAlert({
+                show: true,
+                severity: 'error',
+                message: errorMessage,
+            });
+            formik.setSubmitting(false);
+        }
+    }, [promiseStatus]);
+
+    return (
+        <Box className="OrderForm" component="form" onSubmit={formik.handleSubmit}>
+            <Grid container spacing={2} rowSpacing={1}>
+                <Grid item xs={6}>
+                    <TextField
+                        id="name"
+                        name="name"
+                        variant="outlined"
+                        label="Ім'я"
+                        size="small"
+                        error={formik.touched.name && Boolean(formik.errors.name)}
+                        value={formik.values.name}
+                        onBlur={formik.handleBlur}
+                        onChange={formik.handleChange}
+                        helperText={formik.touched.name && formik.errors.name}
+                        fullWidth
+                    />
+                </Grid>
+                <Grid item xs={6}>
+                    <TextField
+                        id="surname"
+                        name="surname"
+                        variant="outlined"
+                        label="Прізвище"
+                        size="small"
+                        error={formik.touched.surname && Boolean(formik.errors.surname)}
+                        value={formik.values.surname}
+                        onBlur={formik.handleBlur}
+                        onChange={formik.handleChange}
+                        helperText={formik.touched.surname && formik.errors.surname}
+                        fullWidth
+                    />
+                </Grid>
+                <Grid item xs={6}>
+                    <TextField
+                        id="email"
+                        name="email"
+                        variant="outlined"
+                        label="Email"
+                        size="small"
+                        error={formik.touched.email && Boolean(formik.errors.email)}
+                        value={formik.values.email}
+                        onBlur={formik.handleBlur}
+                        onChange={formik.handleChange}
+                        helperText={formik.touched.email && formik.errors.email}
+                        fullWidth
+                    />
+                </Grid>
+                <Grid item xs={6}>
+                    <TextField
+                        id="phoneNumber"
+                        name="phoneNumber"
+                        variant="outlined"
+                        label="Номер телефону"
+                        size="small"
+                        error={formik.touched.phoneNumber && Boolean(formik.errors.phoneNumber)}
+                        value={formik.values.phoneNumber}
+                        onBlur={formik.handleBlur}
+                        onChange={formik.handleChange}
+                        helperText={formik.touched.phoneNumber && formik.errors.phoneNumber}
+                        fullWidth
+                    />
+                </Grid>
+                <Grid item xs={6}>
+                    <TextField
+                        id="address"
+                        name="address"
+                        variant="outlined"
+                        label="Адреса доставки"
+                        size="small"
+                        error={formik.touched.address && Boolean(formik.errors.address)}
+                        value={formik.values.address}
+                        onBlur={formik.handleBlur}
+                        onChange={formik.handleChange}
+                        helperText={formik.touched.address && formik.errors.address}
+                        fullWidth
+                    />
+                </Grid>
+                <Grid item xs={6}>
+                    <TextField
+                        id="delivery"
+                        name="delivery"
+                        variant="outlined"
+                        label="Тип доставкі"
+                        size="small"
+                        extAlign="left"
+                        select
+                        value={formik.values.delivery}
+                        error={formik.touched.delivery && Boolean(formik.errors.delivery)}
+                        onBlur={formik.handleBlur}
+                        onChange={formik.handleChange}
+                        helperText={formik.touched.delivery && formik.errors.delivery}
+                        fullWidth
+                    >
+                        {deliveryOptions.map((option) => (
+                            <MenuItem key={option.value} value={option.value} t>
+                                {option.label}
+                            </MenuItem>
+                        ))}
+                    </TextField>
+                </Grid>
+
+                <Grid item xs={12} display="flex" justifyContent="flex-end">
+                    <Button variant="contained" type="submit" disabled={!formik.isValid || formik.isSubmitting}>
+                        Підтвердити
+                    </Button>
+                </Grid>
+            </Grid>
+        </Box>
+    );
+};
+
+export const COrderForm = connect(
+    (state) => ({
+        promiseStatus: state.promise.orderUpsert?.status || null,
+        serverErrors: state.promise?.orderUpsert?.error || [],
+    }),
+    {
+        onClose: () => actionPromiseClear('orderUpsert'),
+    }
+)(OrderForm);

+ 65 - 0
src/components/CartPage/index.js

@@ -0,0 +1,65 @@
+import { Box, Button, Stack, Table, TableBody, TableCell, TableRow, Typography } from '@mui/material';
+import { useEffect } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import { useNavigate } from 'react-router-dom';
+import { actionOrderUpdate } from '../../actions/actionOrderUpdate';
+import { actionOrderUpsert } from '../../actions/actionOrderUpsert';
+import { actionCartDelete } from '../../reducers';
+import { CartItem } from './CartItem';
+import { COrderForm, OrderForm } from './OrderForm';
+
+export const CartPage = () => {
+    const cart = useSelector((state) => state.cart || {});
+    const sum = Object.entries(cart).reduce((prev, [_id, order]) => prev + order.count * order.good.price, 0);
+    const dispatch = useDispatch();
+    const navigate = useNavigate();
+
+    useEffect(() => {
+        if (!Object.entries(cart).length) {
+            navigate('/');
+        }
+    }, []);
+
+    useEffect(() => {
+        !Object.keys(cart).length && navigate('/');
+    }, [cart]);
+
+    return (
+        <Box className="CartPage">
+            <Stack spacing={2}>
+                <Typography>Оформлення замовлення</Typography>
+                <Table className="table">
+                    <TableBody>
+                        {Object.entries(cart).map(([_id, order]) => (
+                            <CartItem
+                                order={order}
+                                onDeleteClick={(good) => dispatch(actionCartDelete(good))}
+                                key={_id}
+                            />
+                        ))}
+
+                        <TableRow>
+                            <TableCell colSpan={3}>
+                                <Typography variant="body1" bold>
+                                    Всього:
+                                </Typography>
+                            </TableCell>
+                            <TableCell>
+                                <Typography textAlign="center">{sum}</Typography>
+                            </TableCell>
+                            <TableCell></TableCell>
+                        </TableRow>
+                    </TableBody>
+                </Table>
+
+                <COrderForm
+                    onSubmit={(order) => {
+                        const orderToSubmit = order;
+                        orderToSubmit.orderGoods = Object.values(cart);
+                        dispatch(actionOrderUpsert(orderToSubmit));
+                    }}
+                />
+            </Stack>
+        </Box>
+    );
+};

+ 44 - 0
src/components/GoodPage/index.js

@@ -0,0 +1,44 @@
+import { useEffect, useState } from 'react';
+import { CBuyButton } from '../common/BuyButton';
+import { useSelector } from 'react-redux';
+import defaultGoodImage from '../../images/default-good-image.png';
+import { Divider, Grid, Paper, Stack, Table, TableBody, TableCell, TableRow, Typography } from '@mui/material';
+import { Box } from '@mui/system';
+import { Carousel } from 'react-responsive-carousel';
+
+export const GoodPage = () => {
+    const good = useSelector((state) => state.promise?.goodById?.payload || {});
+    const { _id = '', name = '', price = '', description = '', images = [] } = good || {};
+    return (
+        <Box className="GoodPage">
+            <Grid container spacing={4} className="images">
+                <Grid item xs={12} md={4}>
+                    <Carousel showIndicators={false} showStatus={false} showArrows={true}>
+                        {(good.images || [{ url: defaultGoodImage }]).map((image) => (
+                            <img src={image?.url ? `${image?.url}` : defaultGoodImage} />
+                        ))}
+                    </Carousel>
+                </Grid>
+                <Grid item xs={12} md={8} className="content">
+                    <Stack spacing={2}>
+                        <Stack direction="row" alignItems="center" spacing={2}>
+                            <Typography variant="body1" color="#1C1B1F">
+                                <b>Ціна:</b> {price}
+                            </Typography>
+                            <CBuyButton good={good} />
+                        </Stack>
+                        <Divider />
+                        <Typography variant="h5">
+                            <b>Назва: {name}</b>
+                        </Typography>
+                        <Typography variant="body1">
+                            Опис:
+                            <br />
+                            {description}
+                        </Typography>
+                    </Stack>
+                </Grid>
+            </Grid>
+        </Box>
+    );
+};

+ 12 - 0
src/components/GoodsPage/SubCategories/index.js

@@ -0,0 +1,12 @@
+import { Box, Paper } from '@mui/material';
+import { Link } from 'react-router-dom';
+
+export const SubCategories = ({ categories }) => (
+    <Box className="SubCategories">
+        {(categories || []).map((cat) => (
+            <Paper className="SubCategory Link" component={Link} to={`/category/${cat._id}`}>
+                {cat.name}
+            </Paper>
+        ))}
+    </Box>
+);

+ 53 - 0
src/components/GoodsPage/index.js

@@ -0,0 +1,53 @@
+import { Grid, Stack, Typography, Divider } from '@mui/material';
+import { Box } from '@mui/system';
+import { connect, useDispatch } from 'react-redux';
+import { useParams } from 'react-router-dom';
+import { GoodCard } from '../common/GoodCard';
+import { GoodList } from '../common/GoodList';
+import { SubCategories } from './SubCategories';
+import { SortOptions } from '../common/SortOptions';
+import { actionCatById } from '../../actions/actionCatById';
+
+const GoodsPage = ({ category = {} }) => {
+    const { goods = [], name = '', subcategories = [] } = category || {};
+    const dispatch = useDispatch();
+    return (
+        <Box className="GoodsPage">
+            <Box>
+                <Typography variant="h5" textAlign="center">
+                    {name}
+                </Typography>
+            </Box>
+
+            <Divider className="Divider" />
+            <Stack>
+                <Box className="sortOptionsWrapper">
+                    <SortOptions
+                        onClick={(option) =>
+                            category._id && dispatch(actionCatById({ _id: category._id, orderBy: option.value }))
+                        }
+                    />
+                </Box>
+                {!!subcategories.length ? (
+                    <Box>
+                        <Typography variant="h6" color="#79747E" textAlign="left">
+                            Категорії
+                        </Typography>
+                        <SubCategories categories={subcategories} />
+                    </Box>
+                ) : null}
+                {!!goods.length ? (
+                    <Box>
+                        <Typography paddingBottom={1} variant="h6" color="#79747E" textAlign="left">
+                            Товари
+                        </Typography>
+                        <GoodList goods={goods} />
+                    </Box>
+                ) : null}
+            </Stack>
+        </Box>
+    );
+};
+
+const CGoodsPage = connect((state) => ({ category: state?.promise?.catById?.payload || [] }))(GoodsPage);
+export { GoodsPage, CGoodsPage };

+ 87 - 0
src/components/LayoutPage/index.js

@@ -0,0 +1,87 @@
+import { Box, Grid } from '@mui/material';
+import { useEffect } from 'react';
+import { connect, useDispatch } from 'react-redux';
+import { Navigate, Route, Routes, useLocation, useParams } from 'react-router-dom';
+import { actionCatById } from '../../actions/actionCatById';
+import { actionGoodById } from '../../actions/actionGoodById';
+import { actionGoodsFind } from '../../actions/actionGoodsFind';
+import { AdminLayoutPage } from '../admin/AdminLayoutPage';
+import { CartPage } from '../CartPage';
+import { Error404 } from '../common/Error404';
+import { GoodList } from '../common/GoodList';
+import { CProtectedRoute, ProtectedRoute } from '../common/ProtectedRoute';
+import { GoodPage } from '../GoodPage';
+import { CGoodsPage } from '../GoodsPage';
+import { Aside } from '../layout/Aside';
+import Content from '../layout/Content';
+import { Footer } from '../layout/Footer';
+import { Header } from '../layout/Header';
+import { MainPage } from '../MainPage';
+
+const GoodsPageContainer = () => {
+    const params = useParams();
+    const dispatch = useDispatch();
+    if (params._id) {
+        console.log(params._id);
+        dispatch(actionCatById({ _id: params._id }));
+    }
+
+    return <CGoodsPage />;
+};
+
+const GoodPageContainer = () => {
+    const params = useParams();
+    const dispatch = useDispatch();
+
+    dispatch(actionGoodById({ _id: params._id }));
+    return <GoodPage />;
+};
+
+const CGoodsList = connect((state) => ({ goods: state.promise?.pageGoodsFind?.payload || [] }))(GoodList);
+
+const GoodsListContainer = () => {
+    const params = useParams();
+    const dispatch = useDispatch();
+    useEffect(() => {
+        dispatch(actionGoodsFind({ text: params.searchData, promiseName: 'pageGoodsFind' }));
+    }, [params.searchData]);
+
+    return <CGoodsList />;
+};
+
+export const LayoutPage = () => {
+    const location = useLocation();
+    return (
+        <Box className="LayoutPage">
+            <Header />
+            <Grid container columns={14} rows={1}>
+                <Grid xs={location.pathname.match(/(\/categor)|(\/good)|(\/order)*/) ? 3 : 0} item>
+                    <Aside />
+                </Grid>
+                <Grid xs={location.pathname.match(/(\/categor)|(\/good)|(\/order)*/) ? 11 : 14} item>
+                    <Content>
+                        <Routes>
+                            <Route path="/" exact element={<MainPage />} />
+                            <Route path="/cart" exact element={<CartPage />} />
+                            <Route path="/search/:searchData/" element={<GoodsListContainer />} exact />
+                            <Route path="/category/:_id" element={<GoodsPageContainer />} />
+                            <Route path="/category/" element={<GoodsPageContainer />} />
+                            <Route path="/good/:_id" element={<GoodPageContainer />} />
+                            <Route
+                                path="/admin/*"
+                                exact
+                                element={
+                                    <CProtectedRoute roles={['admin']} fallback="/auth">
+                                        <AdminLayoutPage />
+                                    </CProtectedRoute>
+                                }
+                            />
+                            <Route path="*" element={<Navigate to="/404" />} />
+                        </Routes>
+                    </Content>
+                </Grid>
+            </Grid>
+            <Footer />
+        </Box>
+    );
+};

+ 27 - 0
src/components/MainPage/index.js

@@ -0,0 +1,27 @@
+import { Box, Stack, Typography } from '@mui/material';
+import { useEffect } from 'react';
+import { useSelector } from 'react-redux';
+import MainPageImage from '../../images/main-page-image.png';
+import { GoodCardSlider } from '../common/GoodCardSlider';
+
+const MainPage = () => {
+    const popularGoods = useSelector((state) => state.promise?.goodsPopular?.payload || []);
+
+    return (
+        <Box className="MainPage">
+            <Stack spacing={3}>
+                <Box component="img" src={MainPageImage} className="MainPageImage" />
+                <Box>
+                    <Typography variant="h5" color="#79747E" textAlign="left">
+                        Популярні товари
+                    </Typography>
+                </Box>
+                <Box>
+                    <GoodCardSlider goods={popularGoods} />
+                </Box>
+            </Stack>
+        </Box>
+    );
+};
+
+export { MainPage };

+ 36 - 0
src/components/Root/index.js

@@ -0,0 +1,36 @@
+import { Navigate, Route, Routes } from 'react-router-dom';
+
+import { Box } from '@mui/material';
+import styles from 'react-responsive-carousel/lib/styles/carousel.min.css';
+import { actionPageStart } from '../../actions/actionPageStart';
+import { useDispatch, useSelector } from 'react-redux';
+
+import { AuthPage } from '../AuthPage';
+import { LayoutPage } from '../LayoutPage';
+import { CProtectedRoute } from '../common/ProtectedRoute';
+import { Error404 } from '../common/Error404';
+
+const Root = () => {
+    const dispatch = useDispatch();
+    dispatch(actionPageStart());
+
+    return (
+        <Box className="Root">
+            <Routes>
+                <Route
+                    path="/auth"
+                    element={
+                        <CProtectedRoute roles={['anon']} fallback="/admin">
+                            <AuthPage />
+                        </CProtectedRoute>
+                    }
+                />
+                <Route path="/404" element={<Error404 />} />
+                <Route path="/*" element={<LayoutPage />} />
+                <Route path="*" element={<Navigate to="/404" />} />
+            </Routes>
+        </Box>
+    );
+};
+
+export { Root };

+ 48 - 0
src/components/UIContext/index.js

@@ -0,0 +1,48 @@
+import { Alert, Snackbar } from '@mui/material';
+import { createContext, useState } from 'react';
+
+export const UIContext = createContext({});
+
+export const UIContextProvider = ({ children }) => {
+    const [alert, setAlert] = useState({
+        show: false,
+        severity: 'info',
+        message: '',
+    });
+
+    const [snackbar, setSnackbar] = useState({
+        show: false,
+        anchorOrigin: { horizontal: 'center', vertical: 'bottom' },
+        message: '',
+    });
+    const handleAlertClose = () =>
+        setAlert({
+            show: false,
+        });
+
+    const handleSnackbarClose = () => {
+        setSnackbar({
+            show: false,
+        });
+    };
+
+    return (
+        <UIContext.Provider value={{ setAlert, setSnackbar }}>
+            {children}
+            <Snackbar open={alert.show} autoHideDuration={4000} onClose={handleAlertClose}>
+                <Alert elevation={6} variant="filled" severity={alert.severity}>
+                    {alert.message}
+                </Alert>
+            </Snackbar>
+            <Snackbar
+                open={snackbar.show}
+                autoHideDuration={4000}
+                onClose={handleSnackbarClose}
+                anchorOrigin={snackbar.anchorOrigin || { horizontal: 'center', vertical: 'bottom' }}
+                message={snackbar.message}
+            >
+                {snackbar.children}
+            </Snackbar>
+        </UIContext.Provider>
+    );
+};

+ 17 - 0
src/components/admin/AdminCategoriesPage/AdminCategoryItem.js

@@ -0,0 +1,17 @@
+import { Button, TableCell, TableRow } from '@mui/material';
+import { FaEdit } from 'react-icons/fa';
+import { Link } from 'react-router-dom';
+const AdminCategoryItem = ({ category }) => (
+    <TableRow className="AdminCategoryItem">
+        <TableCell scope="row">{category._id}</TableCell>
+        <TableCell>{category.name ? category.name : '-'}</TableCell>
+        <TableCell>{category.parent?.name ? category.parent.name : '-'}</TableCell>
+        <TableCell className="edit">
+            <Button component={Link} className="Link" to={`/admin/category/${category._id}/`} variant="contained">
+                Редагувати
+            </Button>
+        </TableCell>
+    </TableRow>
+);
+
+export { AdminCategoryItem };

+ 57 - 0
src/components/admin/AdminCategoriesPage/AdminCategoryList.js

@@ -0,0 +1,57 @@
+import { AdminCategoryItem } from './AdminCategoryItem';
+import { AdminCategoryListHeader } from './AdminCategoryListHeader';
+import { connect } from 'react-redux';
+import { actionCatsFind } from '../../../actions/actionCatsFind';
+import { actionPromiseClear } from '../../../reducers';
+import { SearchBar, SearchResults } from '../../common/SearchBar';
+import { Box, Table, TableBody, TableHead } from '@mui/material';
+import { useEffect, useState } from 'react';
+import { createSearchParams, useLocation, useNavigate } from 'react-router-dom';
+
+const CSearchBar = connect(null, {
+    onSearch: (text) => actionCatsFind({ promiseName: 'adminCatsFind', text, limit: 5 }),
+    onSearchButtonClick: () => actionPromiseClear('adminCatsFind'),
+})(SearchBar);
+
+const CSearchResults = connect((state) => ({ items: state.promise.adminCatsFind?.payload || [] }))(SearchResults);
+
+const AdminCategoryList = ({ categories, orderBy = '_id' } = {}) => {
+    const navigate = useNavigate();
+    const location = useLocation();
+
+    return (
+        <Box className="AdminCategoryList">
+            <Box className="searchBarWrapper">
+                <CSearchBar
+                    render={CSearchResults}
+                    searchLink="/admin/categories/search/"
+                    renderParams={{ itemLink: '/admin/category/' }}
+                />
+            </Box>
+            <Table>
+                <TableHead>
+                    <AdminCategoryListHeader
+                        sort={orderBy}
+                        onSortChange={(orderBy) => {
+                            navigate({
+                                pathname: location.pathname,
+                                search: createSearchParams({
+                                    orderBy,
+                                }).toString(),
+                            });
+                        }}
+                    />
+                </TableHead>
+                <TableBody>
+                    {(categories || []).map((cat) => (
+                        <AdminCategoryItem category={cat} key={cat._id} />
+                    ))}
+                </TableBody>
+            </Table>
+        </Box>
+    );
+};
+
+const CAdminCategoryList = connect((state) => ({ categories: state.feed?.payload || [] }))(AdminCategoryList);
+
+export { AdminCategoryList, CAdminCategoryList };

+ 50 - 0
src/components/admin/AdminCategoriesPage/AdminCategoryListHeader.js

@@ -0,0 +1,50 @@
+import { connect } from 'react-redux';
+import { AddButton } from '../../common/AddButton';
+
+import { TableCell, TableRow, TableSortLabel } from '@mui/material';
+import { useNavigate } from 'react-router-dom';
+
+const AdminCategoryListHeader = ({ sort, onSortChange }) => {
+    const navigate = useNavigate();
+
+    return (
+        <TableRow className="AdminCategoryListHeader">
+            <TableCell scope="col">
+                <TableSortLabel
+                    active={sort === '_id' || sort === '-_id'}
+                    direction={sort === '_id' ? 'asc' : 'desc'}
+                    onClick={() => onSortChange(sort === '_id' ? '-_id' : '_id')}
+                >
+                    #
+                </TableSortLabel>
+            </TableCell>
+            <TableCell scope="col">
+                <TableSortLabel
+                    active={sort === 'name' || sort === '-name'}
+                    direction={sort === 'name' ? 'asc' : 'desc'}
+                    onClick={() => onSortChange(sort === 'name' ? '-name' : 'name')}
+                >
+                    Назва
+                </TableSortLabel>
+            </TableCell>
+            <TableCell scope="col">
+                <TableSortLabel
+                    active={sort === 'parent' || sort === '-parent'}
+                    direction={sort === 'parent' ? 'asc' : 'desc'}
+                    onClick={() => onSortChange(sort === 'parent' ? '-parent' : 'parent')}
+                >
+                    Батьківська категорія
+                </TableSortLabel>
+            </TableCell>
+            <TableCell scope="col">
+                <AddButton
+                    onClick={() => {
+                        navigate(`/admin/category/`);
+                    }}
+                />
+            </TableCell>
+        </TableRow>
+    );
+};
+
+export { AdminCategoryListHeader };

+ 13 - 0
src/components/admin/AdminCategoriesPage/index.js

@@ -0,0 +1,13 @@
+import { Box, Typography } from '@mui/material';
+import { CAdminCategoryList } from './AdminCategoryList';
+
+export const AdminCategoriesPage = ({ orderBy }) => {
+    return (
+        <Box className="AdminCategoriesPage">
+            <Typography variant="h5" sx={{ marginBottom: '10px', marginTop: '10px' }}>
+                Категорії
+            </Typography>
+            <CAdminCategoryList orderBy={orderBy} />
+        </Box>
+    );
+};

+ 182 - 0
src/components/admin/AdminCategoryPage/CategoryForm.js

@@ -0,0 +1,182 @@
+import { connect } from 'react-redux';
+import React, { useState, useEffect, useContext } from 'react';
+import Select from 'react-select';
+import { actionCategoryUpdate } from '../../../actions/actionCategoryUpdate';
+import { actionPromise, actionPromiseClear, store } from '../../../reducers';
+import { Alert, Box, Button, InputLabel, Snackbar, Stack, TextField, Typography } from '@mui/material';
+import { UIContext } from '../../UIContext';
+import { useFormik } from 'formik';
+import * as Yup from 'yup';
+import { Error } from '../../common/Error';
+
+const categorySchema = Yup.object().shape({
+    name: Yup.string().required("Обов'язкове"),
+});
+
+const CategoryForm = ({
+    serverErrors = [],
+    onSaveClick,
+    onSave,
+    onClose,
+    promiseStatus,
+    catList: initialCatList = [],
+    goodList = [],
+    category = {},
+} = {}) => {
+    const [inputSubcategories, setInputSubcategories] = useState([]);
+    const [inputGoods, setInputGoods] = useState([]);
+    const [inputParent, setInputParent] = useState({});
+    const [subCatList, setSubCatList] = useState([]);
+    const [parentList, setParentList] = useState([]);
+    const { setAlert } = useContext(UIContext);
+
+    const formik = useFormik({
+        initialValues: {
+            name: category?.name || '',
+        },
+        validationSchema: categorySchema,
+        validateOnChange: true,
+        onSubmit: () => {
+            let categoryToSave = {};
+            category?._id && (categoryToSave._id = category?._id);
+            categoryToSave.name = formik.values.name;
+            inputGoods && (categoryToSave.goods = inputGoods);
+            inputParent && (categoryToSave.parent = inputParent);
+
+            categoryToSave.subcategories = inputSubcategories;
+            onSaveClick && onSaveClick();
+            onSave(categoryToSave);
+        },
+    });
+
+    useEffect(() => {
+        formik.setFieldValue('name', category.name || '');
+        setInputParent(category?.parent || null);
+        setInputGoods(category?.goods || []);
+        setInputSubcategories(category?.subcategories || []);
+    }, [category]);
+
+    useEffect(() => {
+        if (promiseStatus === 'FULFILLED') {
+            formik.setSubmitting(false);
+            setAlert({
+                show: true,
+                severity: 'success',
+                message: 'Готово',
+            });
+        }
+        if (promiseStatus === 'REJECTED') {
+            const errorMessage = serverErrors.reduce((prev, curr) => prev + '\n' + curr.message, '');
+            formik.setSubmitting(false);
+            setAlert({
+                show: true,
+                severity: 'error',
+                message: errorMessage,
+            });
+        }
+    }, [promiseStatus]);
+
+    useEffect(() => {
+        let parentList = initialCatList.filter(
+            ({ _id }) =>
+                !category?.subCatergories?.find((subCat) => _id === subCat._id) &&
+                _id !== category?._id &&
+                !inputSubcategories?.find((subCat) => _id === subCat._id)
+        );
+        parentList = [...[{ _id: null, name: 'null' }], ...parentList];
+
+        setParentList(parentList);
+    }, [inputSubcategories]);
+
+    useEffect(() => {
+        let subCatList = initialCatList.filter(
+            ({ _id }) => _id !== category?.parent?._id && _id !== category?._id && inputParent?._id !== _id
+        );
+        setSubCatList(subCatList);
+    }, [inputParent]);
+
+    useEffect(() => {
+        return () => {
+            onClose && onClose();
+        };
+    }, []);
+
+    return (
+        <Box className="CategoryForm" component="form" onSubmit={formik.handleSubmit}>
+            {(serverErrors || []).map((error) => (
+                <Error>{error?.message}</Error>
+            ))}
+
+            <Box>
+                <TextField
+                    id="name"
+                    name="name"
+                    variant="outlined"
+                    label="Назва"
+                    size="small"
+                    error={formik.touched.name && Boolean(formik.errors.name)}
+                    value={formik.values.name}
+                    onBlur={formik.handleBlur}
+                    onChange={formik.handleChange}
+                    helperText={formik.touched.name && formik.errors.name}
+                    multiline
+                    fullWidth
+                    sx={{ mt: 2 }}
+                />
+            </Box>
+            <Box sx={{ mt: 3 }}>
+                <InputLabel className="form-label">Батьківська категорія</InputLabel>
+                <Select
+                    value={{ value: inputParent?._id || null, label: inputParent?.name || 'null' }}
+                    onChange={(e) => setInputParent({ _id: e.value, name: e.label })}
+                    options={parentList.map(({ _id, name }) => ({ value: _id, label: name }))}
+                />
+            </Box>
+
+            <Box sx={{ mt: 3 }}>
+                <InputLabel className="form-label">Підкатегорії</InputLabel>
+                <Select
+                    value={inputSubcategories?.map(({ _id, name }) => ({ value: _id, label: name }))}
+                    closeMenuOnSelect={false}
+                    onChange={(e) => setInputSubcategories(e.map(({ value, label }) => ({ _id: value, name: label })))}
+                    options={subCatList?.map(({ _id, name }) => ({ value: _id, label: name }))}
+                    isMulti={true}
+                />
+            </Box>
+            {
+                <Box sx={{ mt: 3 }}>
+                    <InputLabel className="form-label">Товари</InputLabel>
+                    <Select
+                        value={inputGoods?.map(({ _id, name }) => ({ value: _id, label: name }))}
+                        closeMenuOnSelect={false}
+                        onChange={(e) => setInputGoods(e.map(({ value, label }) => ({ _id: value, name: label })))}
+                        options={goodList?.map(({ _id, name }) => ({ value: _id, label: name }))}
+                        isMulti={true}
+                    />
+                </Box>
+            }
+            <Box direction="row" sx={{ mt: 3 }} justifyContent="flex-end">
+                <Button variant="contained" disabled={!formik.isValid || formik.isSubmitting} type="submit" fullWidth>
+                    Зберегти
+                </Button>
+            </Box>
+        </Box>
+    );
+};
+
+// const CRegisterForm = connect((state) => ({ serverErrors: state.promise?.register?.error || [] }), {
+//     onRegister: (login, password) => actionRegister(login, password),
+// })(RegisterForm);
+
+export const CCategoryForm = connect(
+    (state) => ({
+        catList: state.promise.catAll?.payload || [],
+        promiseStatus: state.promise.categoryUpsert?.status || null,
+        serverErrors: state.promise.categoryUpsert?.error || null,
+        goodList: state.promise.goodsAll?.payload || [],
+    }),
+    {
+        onSave: (cat) => actionCategoryUpdate(cat),
+        onClose: () => actionPromiseClear('categoryUpsert'),
+    }
+)(CategoryForm);

+ 12 - 0
src/components/admin/AdminCategoryPage/index.js

@@ -0,0 +1,12 @@
+import { Box } from '@mui/material';
+import { connect } from 'react-redux';
+import { CCategoryForm } from './CategoryForm';
+
+export const AdminCategoryPage = ({ category }) => (
+    <Box className="AdminCategoryPage">
+        <CCategoryForm category={category} />
+    </Box>
+);
+export const CAdminCategoryPage = connect((state) => ({ category: state.promise?.adminCatById?.payload || {} }))(
+    AdminCategoryPage
+);

+ 221 - 0
src/components/admin/AdminGoodPage/GoodForm.js

@@ -0,0 +1,221 @@
+import { connect } from 'react-redux';
+import React, { useState, useEffect, useContext } from 'react';
+import { actionPromise, actionPromiseClear } from '../../../reducers';
+import Select from 'react-select';
+import { actionGoodUpdate } from '../../../actions/actionGoodUpdate';
+import { EntityEditor } from '../../common/EntityEditor';
+import { actionUploadFiles } from '../../../actions/actionUploadFiles';
+import { UIContext } from '../../UIContext';
+import {
+    Alert,
+    Box,
+    Button,
+    Chip,
+    FormControl,
+    InputLabel,
+    MenuItem,
+    OutlinedInput,
+    Snackbar,
+    Stack,
+    TextareaAutosize,
+    TextField,
+    Typography,
+} from '@mui/material';
+import { useFormik } from 'formik';
+import * as Yup from 'yup';
+import { Error } from '../../common/Error';
+
+const goodSchema = Yup.object().shape({
+    name: Yup.string().required("Обов'язкове"),
+    description: Yup.string().required("Обов'язкове"),
+    price: Yup.number().min(0, 'більше або равно 0').required("Обов'язкове"),
+    amount: Yup.number().min(0, 'більше або равно 0').required("Обов'язкове"),
+});
+
+const CGoodEditor = connect(
+    (state) => ({
+        entity: state.promise?.adminGoodById?.payload || {},
+        uploadFiles: state.promise?.uploadFiles,
+    }),
+    {
+        onFileDrop: (files) => actionUploadFiles(files),
+    }
+)(EntityEditor);
+
+export const GoodForm = ({
+    serverErrors = [],
+    onSaveClick,
+    onSave,
+    onClose,
+    promiseStatus,
+    catList = [],
+    good = {},
+} = {}) => {
+    const [inputCategories, setInputCategories] = useState([]);
+    const [inputImages, setInputImages] = useState([]);
+    const { setAlert } = useContext(UIContext);
+    const formik = useFormik({
+        initialValues: {
+            name: '',
+            description: '',
+            price: 0,
+            amount: 0,
+        },
+        validationSchema: goodSchema,
+        validateOnChange: true,
+        onSubmit: () => {
+            let goodToSave = {};
+            good?._id && (goodToSave._id = good._id);
+            goodToSave.name = formik.values.name;
+            goodToSave.description = formik.values.description;
+            goodToSave.price = +formik.values.price;
+            goodToSave.amount = +formik.values.amount;
+            goodToSave.categories = inputCategories;
+            goodToSave.images = inputImages?.map(({ _id }) => ({ _id })) || [];
+            onSaveClick && onSaveClick();
+            onSave(goodToSave);
+        },
+    });
+
+    useEffect(() => {
+        if (promiseStatus === 'FULFILLED') {
+            formik.setSubmitting(false);
+            setAlert({
+                show: true,
+                severity: 'success',
+                message: 'Готово',
+            });
+        }
+        if (promiseStatus === 'REJECTED') {
+            const errorMessage = serverErrors.reduce((prev, curr) => prev + '\n' + curr.message, '');
+            formik.setSubmitting(false);
+            setAlert({
+                show: true,
+                severity: 'error',
+                message: errorMessage,
+            });
+        }
+    }, [promiseStatus]);
+
+    useEffect(() => {
+        setInputCategories(good?.categories || []);
+        setInputImages(good?.images || []);
+        formik.setFieldValue('name', good.name || '');
+        formik.setFieldValue('description', good.description || '');
+        formik.setFieldValue('amount', good.amount || 0);
+        formik.setFieldValue('price', good.price || 0);
+    }, [good.categories, good.name, good.description, good.amount, good.price]);
+
+    useEffect(() => {
+        return () => {
+            onClose && onClose();
+        };
+    }, []);
+    return (
+        <Box className="GoodForm" component="form" onSubmit={formik.handleSubmit}>
+            <TextField
+                id="name"
+                name="name"
+                variant="outlined"
+                label="Назва"
+                size="small"
+                error={formik.touched.name && Boolean(formik.errors.name)}
+                value={formik.values.name}
+                onBlur={formik.handleBlur}
+                onChange={formik.handleChange}
+                helperText={formik.touched.name && formik.errors.name}
+                multiline
+                fullWidth
+                sx={{ mt: 2 }}
+            />
+
+            <Box sx={{ mt: 3 }}>
+                <InputLabel>Зображення</InputLabel>
+                <CGoodEditor onImagesSave={(images) => setInputImages(images)} />
+            </Box>
+
+            <TextField
+                variant="outlined"
+                id="description"
+                name="description"
+                label="Опис"
+                size="small"
+                error={formik.touched.description && Boolean(formik.errors.description)}
+                value={formik.values.description}
+                onBlur={formik.handleBlur}
+                onChange={formik.handleChange}
+                helperText={formik.touched.description && formik.errors.description}
+                multiline
+                fullWidth
+                sx={{ mt: 2 }}
+            />
+
+            <Box sx={{ mt: 3 }}>
+                <TextField
+                    variant="outlined"
+                    id="price"
+                    name="price"
+                    label="Ціна"
+                    size="small"
+                    error={formik.touched.price && Boolean(formik.errors.price)}
+                    value={formik.values.price}
+                    onBlur={formik.handleBlur}
+                    onChange={formik.handleChange}
+                    helperText={formik.touched.price && formik.errors.price}
+                    multiline
+                    fullWidth
+                    sx={{ mt: 2 }}
+                />
+            </Box>
+
+            <Box sx={{ mt: 3 }}>
+                <TextField
+                    variant="outlined"
+                    id="amount"
+                    name="amount"
+                    label="Кількість"
+                    size="small"
+                    error={formik.touched.amount && Boolean(formik.errors.amount)}
+                    value={formik.values.amount}
+                    onBlur={formik.handleBlur}
+                    onChange={formik.handleChange}
+                    helperText={formik.touched.amount && formik.errors.amount}
+                    multiline
+                    fullWidth
+                    sx={{ mt: 2 }}
+                />
+            </Box>
+
+            <Box sx={{ mt: 3 }}>
+                <InputLabel>Категорії</InputLabel>
+                <Select
+                    placeholder="Обрати категорії"
+                    value={inputCategories.map(({ _id, name }) => ({ value: _id, label: name }))}
+                    closeMenuOnSelect={false}
+                    onChange={(e) => setInputCategories(e.map(({ label, value }) => ({ _id: value, name: label })))}
+                    options={catList?.map(({ _id, name }) => ({ value: _id, label: name }))}
+                    isMulti={true}
+                />
+            </Box>
+
+            <Box direction="row" sx={{ mt: 3 }} justifyContent="flex-end">
+                <Button variant="contained" disabled={!formik.isValid || formik.isSubmitting} type="submit" fullWidth>
+                    Зберегти
+                </Button>
+            </Box>
+        </Box>
+    );
+};
+
+export const CGoodForm = connect(
+    (state) => ({
+        catList: state.promise.catAll?.payload || [],
+        promiseStatus: state.promise.goodUpsert?.status || null,
+        good: state.promise?.adminGoodById?.payload || {},
+        serverErrors: state.promise?.goodUpsert?.error || [],
+    }),
+    {
+        onSave: (good) => actionGoodUpdate(good),
+        onClose: () => actionPromiseClear('goodUpsert'),
+    }
+)(GoodForm);

+ 12 - 0
src/components/admin/AdminGoodPage/index.js

@@ -0,0 +1,12 @@
+import { Box } from '@mui/material';
+import { CGoodForm } from './GoodForm';
+import { connect } from 'react-redux';
+
+export const AdminGoodPage = ({ good }) => (
+    <Box className="AdminGoodPage">
+        <CGoodForm good={good} />
+    </Box>
+);
+export const CAdminGoodPage = connect((state) => ({ good: state.promise?.adminGoodById?.payload || {} }))(
+    AdminGoodPage
+);

+ 35 - 0
src/components/admin/AdminGoodsPage/AdminGoodItem.js

@@ -0,0 +1,35 @@
+import { actionPopupOpen } from '../../../reducers';
+import { connect } from 'react-redux';
+import { Link } from 'react-router-dom';
+import defaultGoodImage from '../../../images/default-good-image.png';
+import { FaEdit } from 'react-icons/fa';
+import { Box, Button, TableCell, TableRow } from '@mui/material';
+
+const AdminGoodItem = ({ good }) => (
+    <TableRow className="AdminGoodItem">
+        <TableCell scope="row">{good._id}</TableCell>
+        <TableCell>{good.name ? good.name : '-'}</TableCell>
+        <TableCell>
+            {
+                <Box
+                    component="img"
+                    src={good.images?.length ? `${good.images ? good.images[0]?.url : ''}` : defaultGoodImage}
+                />
+            }
+        </TableCell>
+        <TableCell>{good.price ? good.price : '-'}</TableCell>
+        <TableCell>{good.amount ? good.amount : '-'}</TableCell>
+        <TableCell>
+            {good.categories
+                ? (good.categories || []).map((category) => <div key={category._id}>{category?.name}</div>)
+                : '-'}
+        </TableCell>
+        <TableCell className="edit">
+            <Button component={Link} className="Link" to={`/admin/good/${good._id}/`} variant="contained">
+                Редагувати
+            </Button>
+        </TableCell>
+    </TableRow>
+);
+
+export { AdminGoodItem };

+ 58 - 0
src/components/admin/AdminGoodsPage/AdminGoodList.js

@@ -0,0 +1,58 @@
+import { AdminGoodListHeader } from './AdminGoodListHeader';
+import { AdminGoodItem } from './AdminGoodItem';
+import { connect } from 'react-redux';
+import { useEffect, useState } from 'react';
+
+import { SearchBar, SearchResults } from '../../common/SearchBar';
+import { actionGoodsFind } from '../../../actions/actionGoodsFind';
+import { actionPromiseClear } from '../../../reducers';
+import { Box, Table, TableBody, TableHead } from '@mui/material';
+import { createSearchParams, useLocation, useNavigate } from 'react-router-dom';
+
+const CSearchBar = connect(null, {
+    onSearch: (text) => actionGoodsFind({ promiseName: 'adminGoodsFind', text, limit: 5 }),
+    onSearchButtonClick: () => actionPromiseClear('adminGoodsFind'),
+})(SearchBar);
+
+const CSearchResults = connect((state) => ({ items: state.promise.adminGoodsFind?.payload || [] }))(SearchResults);
+
+const AdminGoodList = ({ goods, orderBy = '_id' }) => {
+    const navigate = useNavigate();
+    const location = useLocation();
+
+    return (
+        <Box className="AdminGoodList">
+            <Box className="searchBarWrapper">
+                <CSearchBar
+                    render={CSearchResults}
+                    searchLink="/admin/goods/search/"
+                    renderParams={{ itemLink: '/admin/good/' }}
+                />
+            </Box>
+            <Table>
+                <TableHead>
+                    <AdminGoodListHeader
+                        sort={orderBy}
+                        onSortChange={(orderBy) => {
+                            navigate({
+                                pathname: location.pathname,
+                                search: createSearchParams({
+                                    orderBy,
+                                }).toString(),
+                            });
+                        }}
+                    />
+                </TableHead>
+                <TableBody>
+                    {(goods || []).map((good) => (
+                        <AdminGoodItem good={good} key={good._id} />
+                    ))}
+                </TableBody>
+            </Table>
+        </Box>
+    );
+};
+
+const CAdminGoodList = connect((state) => ({ goods: state.feed?.payload || [] }))(AdminGoodList);
+
+export { AdminGoodList, CAdminGoodList };

+ 61 - 0
src/components/admin/AdminGoodsPage/AdminGoodListHeader.js

@@ -0,0 +1,61 @@
+import { connect } from 'react-redux';
+
+import { AddButton } from '../../common/AddButton';
+import { TableCell, TableRow, TableSortLabel } from '@mui/material';
+import { useNavigate } from 'react-router-dom';
+
+const AdminGoodListHeader = ({ onSortChange, sort }) => {
+    const navigate = useNavigate();
+
+    return (
+        <TableRow className="AdminGoodListHeader">
+            <TableCell scope="col">
+                <TableSortLabel
+                    active={sort === '_id' || sort === '-_id'}
+                    direction={sort === '_id' ? 'asc' : 'desc'}
+                    onClick={() => onSortChange(sort === '_id' ? '-_id' : '_id')}
+                >
+                    #
+                </TableSortLabel>
+            </TableCell>
+            <TableCell scope="col">
+                <TableSortLabel
+                    active={sort === 'name' || sort === '-name'}
+                    direction={sort === 'name' ? 'asc' : 'desc'}
+                    onClick={() => onSortChange(sort === 'name' ? '-name' : 'name')}
+                >
+                    Назва
+                </TableSortLabel>
+            </TableCell>
+            <TableCell scope="col">Зображення</TableCell>
+            <TableCell scope="col">
+                <TableSortLabel
+                    active={sort === 'price' || sort === '-price'}
+                    direction={sort === 'price' ? 'asc' : 'desc'}
+                    onClick={() => onSortChange(sort === 'price' ? '-price' : 'price')}
+                >
+                    Ціна
+                </TableSortLabel>
+            </TableCell>
+            <TableCell scope="col">
+                <TableSortLabel
+                    active={sort === 'amount' || sort === '-amount'}
+                    direction={sort === 'amount' ? 'asc' : 'desc'}
+                    onClick={() => onSortChange(sort === 'amount' ? '-amount' : 'amount')}
+                >
+                    Кількість
+                </TableSortLabel>
+            </TableCell>
+            <TableCell scope="col">Категорії</TableCell>
+            <TableCell scope="col">
+                <AddButton
+                    onClick={() => {
+                        navigate('/admin/good/');
+                    }}
+                />
+            </TableCell>
+        </TableRow>
+    );
+};
+
+export { AdminGoodListHeader };

+ 13 - 0
src/components/admin/AdminGoodsPage/index.js

@@ -0,0 +1,13 @@
+import { Box, Typography } from '@mui/material';
+import { AdminGoodList, CAdminGoodList } from './AdminGoodList';
+
+export const AdminGoodsPage = ({ orderBy }) => {
+    return (
+        <Box className="AdminGoodsPage">
+            <Typography variant="h5" sx={{ marginBottom: '10px', marginTop: '10px' }}>
+                Товари
+            </Typography>
+            <CAdminGoodList orderBy={orderBy} />
+        </Box>
+    );
+};

+ 216 - 0
src/components/admin/AdminLayoutPage/index.js

@@ -0,0 +1,216 @@
+import { Box, Container } from '@mui/material';
+import { useEffect } from 'react';
+import { connect, useDispatch, useSelector } from 'react-redux';
+import { Navigate, Route, Routes, useParams, useSearchParams } from 'react-router-dom';
+import { actionGoodById } from '../../../actions/actionGoodById';
+import { actionCatById } from '../../../actions/actionCatById';
+import { actionPromiseClear, store, actionFeedCats } from '../../../reducers';
+import { actionFeedAdd, actionFeedClear, actionFeedGoods, actionFeedOrders } from '../../../reducers/feedReducer';
+import { CProtectedRoute } from '../../common/ProtectedRoute';
+import { CAdminGoodPage } from '../AdminGoodPage';
+import { AdminGoodsPage } from '../AdminGoodsPage';
+import { AdminCategoriesPage } from '../AdminCategoriesPage';
+import { CAdminCategoryPage } from '../AdminCategoryPage';
+import { AdminOrdersPage } from '../AdminOrdersPage';
+import { CAdminOrderPage } from '../AdminOrderPage';
+import { actionOrderById } from '../../../actions/actionOrderById';
+import { actionCatAll } from '../../../actions/actionCatAll';
+import { actionGoodsAll } from '../../../actions/actionGoodsAll';
+
+const AdminCategoryPageContainer = ({}) => {
+    const dispatch = useDispatch();
+    const params = useParams();
+    dispatch(actionGoodsAll());
+    useEffect(() => {
+        if (params._id) {
+            dispatch(actionCatById({ _id: params._id, promiseName: 'adminCatById' }));
+        } else {
+            dispatch(actionPromiseClear('adminCatById'));
+        }
+    }, [params._id]);
+    return <CAdminCategoryPage />;
+};
+
+const AdminCategoriesPageContainer = ({ cats }) => {
+    const dispatch = useDispatch();
+    const [searchParams] = useSearchParams();
+    const orderBy = searchParams.get('orderBy') || '_id';
+
+    useEffect(() => {
+        dispatch(actionFeedClear());
+        dispatch(actionPromiseClear('feedCatAll'));
+        dispatch(actionPromiseClear('categoryUpsert'));
+        dispatch(actionFeedCats({ skip: 0, orderBy }));
+    }, [orderBy]);
+
+    useEffect(() => {
+        dispatch(actionFeedCats({ skip: cats?.length || 0, orderBy }));
+        window.onscroll = (e) => {
+            if (window.innerHeight + window.scrollY >= document.body.offsetHeight) {
+                const {
+                    feed,
+                    promise: { feedCatAll },
+                } = store.getState();
+                if (feedCatAll.status !== 'PENDING') {
+                    dispatch(actionFeedCats(feed.payload?.length || 0, orderBy));
+                }
+            }
+        };
+        return () => {
+            dispatch(actionFeedClear());
+            dispatch(actionPromiseClear('feedCatAll'));
+            dispatch(actionPromiseClear('categoryUpsert'));
+            window.onscroll = null;
+        };
+    }, []);
+
+    useEffect(() => {
+        if (cats.length) dispatch(actionFeedAdd(cats));
+    }, [cats]);
+
+    return <AdminCategoriesPage orderBy={orderBy} />;
+};
+
+const AdminGoodPageContainer = () => {
+    const params = useParams();
+    const dispatch = useDispatch();
+    dispatch(actionCatAll());
+    useEffect(() => {
+        if (params._id) {
+            dispatch(actionGoodById({ _id: params._id, promiseName: 'adminGoodById' }));
+        } else {
+            dispatch(actionPromiseClear('adminGoodById'));
+        }
+    }, [params._id]);
+    return <CAdminGoodPage />;
+};
+
+const AdminGoodsPageContainer = ({ goods }) => {
+    const dispatch = useDispatch();
+    const [searchParams] = useSearchParams();
+    const orderBy = searchParams.get('orderBy') || '_id';
+
+    useEffect(() => {
+        dispatch(actionFeedClear());
+        dispatch(actionPromiseClear('feedGoodsAll'));
+        dispatch(actionPromiseClear('goodUpsert'));
+        dispatch(actionFeedGoods({ skip: 0, orderBy }));
+    }, [orderBy]);
+
+    useEffect(() => {
+        dispatch(actionFeedGoods({ skip: goods?.length || 0, orderBy }));
+        window.onscroll = (e) => {
+            if (window.innerHeight + window.scrollY >= document.body.offsetHeight) {
+                const {
+                    feed,
+                    promise: { feedGoodsAll },
+                } = store.getState();
+
+                if (feedGoodsAll.status !== 'PENDING') {
+                    dispatch(actionFeedGoods({ skip: feed.payload?.length || 0, orderBy }));
+                }
+            }
+        };
+        return () => {
+            dispatch(actionFeedClear());
+            dispatch(actionPromiseClear('feedGoodsAll'));
+            dispatch(actionPromiseClear('goodUpsert'));
+            window.onscroll = null;
+        };
+    }, []);
+
+    useEffect(() => {
+        if (goods?.length) store.dispatch(actionFeedAdd(goods));
+    }, [goods]);
+    return <AdminGoodsPage orderBy={orderBy} />;
+};
+
+const AdminOrdersPageContainer = ({ orders }) => {
+    const dispatch = useDispatch();
+    const [searchParams] = useSearchParams();
+    const orderBy = searchParams.get('orderBy') || '_id';
+    const status = searchParams.get('status') || 0;
+
+    useEffect(() => {
+        dispatch(actionFeedClear());
+        dispatch(actionPromiseClear('feedOrdersAll'));
+        dispatch(actionPromiseClear('orderUpsert'));
+        dispatch(actionFeedOrders({ skip: 0, orderBy, status }));
+    }, [orderBy, status]);
+
+    useEffect(() => {
+        dispatch(actionFeedOrders({ skip: orders?.length || 0, orderBy, status }));
+        window.onscroll = (e) => {
+            if (window.innerHeight + window.scrollY >= document.body.offsetHeight) {
+                const {
+                    feed,
+                    promise: { feedOrdersAll },
+                } = store.getState();
+
+                if (feedOrdersAll.status !== 'PENDING') {
+                    dispatch(actionFeedOrders({ skip: feed.payload?.length || 0, orderBy, status }));
+                }
+            }
+        };
+        return () => {
+            dispatch(actionFeedClear());
+            dispatch(actionPromiseClear('feedOrdersAll'));
+            dispatch(actionPromiseClear('orderUpsert'));
+            window.onscroll = null;
+        };
+    }, []);
+
+    useEffect(() => {
+        if (orders?.length) store.dispatch(actionFeedAdd(orders));
+    }, [orders]);
+    return <AdminOrdersPage orderBy={orderBy} />;
+};
+
+const AdminOrderPageContainer = () => {
+    const params = useParams();
+    const dispatch = useDispatch();
+    dispatch(actionPromiseClear('adminOrderById'));
+    dispatch(actionGoodsAll());
+    useEffect(() => {
+        if (params._id) {
+            dispatch(actionOrderById({ _id: params._id, promiseName: 'adminOrderById' }));
+        } else {
+            dispatch(actionOrderById('adminOrderById'));
+        }
+    }, [params._id]);
+    return <CAdminOrderPage />;
+};
+
+const CAdminGoodsPageContainer = connect((state) => ({ goods: state.promise?.feedGoodsAll?.payload || [] }))(
+    AdminGoodsPageContainer
+);
+
+const CAdminOrdersPageContainer = connect((state) => ({ orders: state.promise?.feedOrdersAll?.payload || [] }))(
+    AdminOrdersPageContainer
+);
+
+const CAdminCategoriesPageContainer = connect((state) => ({ cats: state.promise?.feedCatAll?.payload || [] }))(
+    AdminCategoriesPageContainer
+);
+
+const AdminLayoutPage = () => {
+    return (
+        <Box className="AdminLayoutPage">
+            <Routes>
+                <Route path="/" element={<Navigate to={'/admin/goods/'} />} exact />
+                <Route path="/goods/" element={<CAdminGoodsPageContainer />} />
+                <Route path="/good/" element={<AdminGoodPageContainer />} />
+                <Route path="/good/:_id" element={<AdminGoodPageContainer />} />
+                <Route path="/categories/" element={<CAdminCategoriesPageContainer />} />
+                <Route path="/category/" element={<AdminCategoryPageContainer />} />
+                <Route path="/category/:_id" element={<AdminCategoryPageContainer />} />
+                <Route path="/orders/" element={<CAdminOrdersPageContainer />} />
+                <Route path="/order/" element={<AdminOrderPageContainer />} />
+                <Route path="/order/:_id" element={<AdminOrderPageContainer />} />
+                <Route path="*" element={<Navigate to="/404" />} />
+            </Routes>
+        </Box>
+    );
+};
+
+export { AdminLayoutPage };

+ 272 - 0
src/components/admin/AdminOrderPage/OrderForm.js

@@ -0,0 +1,272 @@
+import { connect, useSelector } from 'react-redux';
+import React, { useState, useEffect, useContext } from 'react';
+import { actionPromise, actionPromiseClear } from '../../../reducers';
+import Select from 'react-select';
+import { actionOrderUpdate } from '../../../actions/actionOrderUpdate';
+import { EntityEditor } from '../../common/EntityEditor';
+import { actionUploadFiles } from '../../../actions/actionUploadFiles';
+import { UIContext } from '../../UIContext';
+import {
+    Box,
+    Button,
+    Chip,
+    FormControl,
+    Grid,
+    InputLabel,
+    MenuItem,
+    OutlinedInput,
+    Stack,
+    TextareaAutosize,
+    TextField,
+    Typography,
+} from '@mui/material';
+import { FormikProvider, useFormik } from 'formik';
+import * as Yup from 'yup';
+import { Error } from '../../common/Error';
+import { statusNumber, statusOptions } from '../../../helpers';
+import { OrderGoodsEditor } from './OrderGoodsEditor';
+
+const deliveryOptions = [
+    { label: 'Нова пошта', value: 'nova-poshta' },
+    { label: 'Justin', value: 'justin' },
+];
+
+const orderSchema = Yup.object().shape({
+    email: Yup.string().required("Обов'язкове"),
+    phoneNumber: Yup.string().required("Обов'язкове"),
+    name: Yup.string(),
+    address: Yup.string().required("Обов'язкове"),
+    surname: Yup.string(),
+    delivery: Yup.string()
+        .required("обов'язкове")
+        .oneOf(
+            deliveryOptions.map((option) => option.value),
+            'не знайдено'
+        ),
+});
+
+export const OrderForm = ({ serverErrors = [], onSaveClick, onSave, onClose, promiseStatus, order = {} } = {}) => {
+    const [inputStatus, setInputStatus] = useState(null);
+    const { setAlert } = useContext(UIContext);
+    const goodList = useSelector((state) => state.promise?.goodsAll?.payload || []);
+    const [inputOrderGoods, setInputOrderGoods] = useState([]);
+
+    const formik = useFormik({
+        initialValues: {
+            email: '',
+            name: '',
+            surname: '',
+            phoneNumber: '',
+            delivery: '',
+            address: '',
+        },
+        validationSchema: orderSchema,
+        validateOnChange: true,
+        onSubmit: () => {
+            let orderToSave = {};
+            order?._id && (orderToSave._id = order._id);
+            orderToSave.name = formik.values.name;
+            orderToSave.email = formik.values.email;
+            orderToSave.status = inputStatus;
+            orderToSave.surname = formik.values.surname;
+            orderToSave.phoneNumber = formik.values.phoneNumber;
+            orderToSave.address = formik.values.address;
+            orderToSave.delivery = formik.values.delivery;
+            orderToSave.orderGoods = inputOrderGoods;
+            onSaveClick && onSaveClick();
+            onSave(orderToSave);
+        },
+    });
+
+    useEffect(() => {
+        setInputStatus(order?.status || null);
+        setInputOrderGoods(order.orderGoods || []);
+        formik.setFieldValue('email', order.email || '');
+        formik.setFieldValue('name', order.name || '');
+        formik.setFieldValue('address', order.address || '');
+        formik.setFieldValue('surname', order.surname || '');
+        formik.setFieldValue('phoneNumber', order.phoneNumber || '');
+        formik.setFieldValue('delivery', order.delivery || '');
+    }, [order]);
+
+    useEffect(() => {
+        formik.validateForm();
+    }, [formik.values]);
+
+    useEffect(() => {
+        if (promiseStatus === 'FULFILLED') {
+            formik.setSubmitting(false);
+            setAlert({
+                show: true,
+                severity: 'success',
+                message: 'Готово',
+            });
+        }
+        if (promiseStatus === 'REJECTED') {
+            const errorMessage = serverErrors.reduce((prev, curr) => prev + '\n' + curr.message, '');
+            formik.setSubmitting(false);
+            setAlert({
+                show: true,
+                severity: 'error',
+                message: errorMessage,
+            });
+        }
+    }, [promiseStatus]);
+
+    useEffect(() => {
+        return () => {
+            onClose && onClose();
+        };
+    }, []);
+    return (
+        <Box className="OrderForm" component="form" onSubmit={formik.handleSubmit}>
+            {(serverErrors || []).map((error) => (
+                <Error>{error?.message}</Error>
+            ))}
+            <Grid container spacing={5}>
+                <Grid item xs={6}>
+                    <TextField
+                        id="email"
+                        name="email"
+                        variant="outlined"
+                        label="Email"
+                        size="small"
+                        error={formik.touched.email && Boolean(formik.errors.email)}
+                        value={formik.values.email}
+                        onBlur={formik.handleBlur}
+                        onChange={formik.handleChange}
+                        helperText={formik.touched.email && formik.errors.email}
+                        fullWidth
+                        sx={{ mt: 2 }}
+                    />
+                    <TextField
+                        id="name"
+                        name="name"
+                        variant="outlined"
+                        label="Ім'я"
+                        size="small"
+                        error={formik.touched.name && Boolean(formik.errors.name)}
+                        value={formik.values.name}
+                        onBlur={formik.handleBlur}
+                        onChange={formik.handleChange}
+                        helperText={formik.touched.name && formik.errors.name}
+                        fullWidth
+                        sx={{ mt: 2 }}
+                    />
+
+                    <Box sx={{ mt: 3 }}>
+                        <InputLabel className="form-label">Статус</InputLabel>
+                        <Select
+                            value={{
+                                value: inputStatus || null,
+                                label: inputStatus ? statusNumber[inputStatus] : null,
+                            }}
+                            onChange={(e) => setInputStatus(e.value)}
+                            options={statusOptions}
+                        />
+                    </Box>
+
+                    <Box sx={{ mt: 3 }}>
+                        <InputLabel className="form-label">Товари</InputLabel>
+                        <OrderGoodsEditor
+                            orderGoods={inputOrderGoods}
+                            goodList={goodList}
+                            onChange={(orderGoods) => {
+                                setInputOrderGoods([...orderGoods]);
+                            }}
+                        />
+                    </Box>
+
+                    <Box direction="row" sx={{ mt: 3 }} justifyContent="flex-end">
+                        <Button
+                            variant="contained"
+                            disabled={Boolean(!formik.isValid || formik.isSubmitting)}
+                            type="submit"
+                            fullWidth
+                        >
+                            Зберегти
+                        </Button>
+                    </Box>
+                </Grid>
+                <Grid item xs={6}>
+                    <TextField
+                        variant="outlined"
+                        id="phoneNumber"
+                        name="phoneNumber"
+                        label="Номер"
+                        size="small"
+                        error={formik.touched.phoneNumber && Boolean(formik.errors.phoneNumber)}
+                        value={formik.values.phoneNumber}
+                        onBlur={formik.handleBlur}
+                        onChange={formik.handleChange}
+                        helperText={formik.touched.phoneNumber && formik.errors.phoneNumber}
+                        multiline
+                        fullWidth
+                        sx={{ mt: 2 }}
+                    />
+                    <TextField
+                        id="surname"
+                        name="surname"
+                        variant="outlined"
+                        label="Прізвище"
+                        size="small"
+                        error={formik.touched.surname && Boolean(formik.errors.surname)}
+                        value={formik.values.surname}
+                        onBlur={formik.handleBlur}
+                        onChange={formik.handleChange}
+                        helperText={formik.touched.surname && formik.errors.surname}
+                        fullWidth
+                        sx={{ mt: 2 }}
+                    />
+                    <TextField
+                        id="address"
+                        name="address"
+                        variant="outlined"
+                        label="Адреса"
+                        size="small"
+                        error={formik.touched.address && Boolean(formik.errors.address)}
+                        value={formik.values.address}
+                        onBlur={formik.handleBlur}
+                        onChange={formik.handleChange}
+                        helperText={formik.touched.address && formik.errors.address}
+                        fullWidth
+                        sx={{ mt: 2 }}
+                    />
+                    <TextField
+                        id="delivery"
+                        name="delivery"
+                        variant="outlined"
+                        label="Тип доставкі"
+                        size="small"
+                        select
+                        error={formik.touched.delivery && Boolean(formik.errors.delivery)}
+                        value={formik.values.delivery}
+                        onBlur={formik.handleBlur}
+                        onChange={formik.handleChange}
+                        helperText={formik.touched.delivery && formik.errors.delivery}
+                        fullWidth
+                        sx={{ mt: 2 }}
+                    >
+                        {deliveryOptions.map((option) => (
+                            <MenuItem key={option.value} value={option.value} t>
+                                {option.label}
+                            </MenuItem>
+                        ))}
+                    </TextField>
+                </Grid>
+            </Grid>
+        </Box>
+    );
+};
+
+export const COrderForm = connect(
+    (state) => ({
+        promiseStatus: state.promise.orderUpsert?.status || null,
+        serverErrors: state.promise.orderUpsert?.error || null,
+        order: state.promise?.adminOrderById?.payload || {},
+    }),
+    {
+        onSave: (order) => actionOrderUpdate(order),
+        onClose: () => actionPromiseClear('orderUpsert'),
+    }
+)(OrderForm);

+ 31 - 0
src/components/admin/AdminOrderPage/OrderGoodsEditor/OrderGood.js

@@ -0,0 +1,31 @@
+import { IconButton, Stack, TableCell, TableRow, Typography } from '@mui/material';
+import { AiOutlineMinus, AiOutlinePlus } from 'react-icons/ai';
+
+export const OrderGood = ({ orderGood, onChange }) => {
+    return (
+        <TableRow>
+            <TableCell>{orderGood.good.name}</TableCell>
+            <TableCell>
+                <Stack justifyContent="center" direction="row" alignItems="center">
+                    <IconButton
+                        onClick={() => {
+                            orderGood.count -= 1;
+                            onChange(orderGood);
+                        }}
+                    >
+                        <AiOutlineMinus />
+                    </IconButton>
+                    <Typography>{orderGood.count}</Typography>
+                    <IconButton
+                        onClick={() => {
+                            orderGood.count += 1;
+                            onChange(orderGood);
+                        }}
+                    >
+                        <AiOutlinePlus />
+                    </IconButton>
+                </Stack>
+            </TableCell>
+        </TableRow>
+    );
+};

+ 68 - 0
src/components/admin/AdminOrderPage/OrderGoodsEditor/index.js

@@ -0,0 +1,68 @@
+import { Box, Table, TableBody } from '@mui/material';
+import { OrderGood } from './OrderGood';
+import Select from 'react-select';
+import { useEffect, useState } from 'react';
+
+export const OrderGoodsEditor = ({ orderGoods = [], onChange = null, goodList = [] } = {}) => {
+    const handleChange = (goods) => {
+        const deltaGoods = goods.filter(
+            (good) =>
+                !orderGoods.some((orderGood) => {
+                    return good._id === orderGood.good._id;
+                })
+        );
+
+        const deltaOrderGoods = orderGoods.filter(
+            (orderGood) =>
+                !goods.some((good) => {
+                    return good._id === orderGood.good._id;
+                })
+        );
+
+        for (const good of deltaGoods) {
+            orderGoods.push({ count: 1, good: good });
+        }
+        for (const [key, value] of Object.entries(deltaOrderGoods)) {
+            orderGoods = orderGoods.filter((orderGood) => {
+                return orderGood.good._id !== value.good._id;
+            });
+        }
+
+        onChange(orderGoods);
+    };
+
+    return (
+        <Box classname="OrderGoodsEditor">
+            <Select
+                placeholder="Обрати товари"
+                value={orderGoods.map((orderGood) => ({ value: orderGood.good._id, label: orderGood.good.name }))}
+                closeMenuOnSelect={false}
+                onChange={(e) => {
+                    handleChange(e.map(({ label, value }) => ({ _id: value, name: label })));
+                }}
+                options={goodList?.map(({ _id, name }) => ({ value: _id, label: name }))}
+                isMulti={true}
+            />
+            <Table>
+                <TableBody>
+                    {(orderGoods || []).map((orderGood, idx) => (
+                        <OrderGood
+                            key={orderGood.good._id}
+                            orderGood={orderGood}
+                            onChange={(newOrderGood) => {
+                                if (+newOrderGood.count <= 0) {
+                                    orderGoods = orderGoods.filter((item, index) => {
+                                        return idx !== index;
+                                    });
+                                } else {
+                                    orderGoods[idx] = orderGood;
+                                }
+                                onChange(orderGoods);
+                            }}
+                        />
+                    ))}
+                </TableBody>
+            </Table>
+        </Box>
+    );
+};

+ 12 - 0
src/components/admin/AdminOrderPage/index.js

@@ -0,0 +1,12 @@
+import { Box } from '@mui/material';
+import { COrderForm } from './OrderForm';
+import { connect } from 'react-redux';
+
+export const AdminOrderPage = ({ order }) => (
+    <Box className="AdminOrderPage">
+        <COrderForm order={order} />
+    </Box>
+);
+export const CAdminOrderPage = connect((state) => ({ order: state.promise?.adminOrderById?.payload || {} }))(
+    AdminOrderPage
+);

+ 30 - 0
src/components/admin/AdminOrdersPage/AdminOrderItem.js

@@ -0,0 +1,30 @@
+import { Link } from 'react-router-dom';
+
+import { Box, Button, TableCell, TableRow, Typography } from '@mui/material';
+import { statusNumber } from '../../../helpers';
+
+const AdminOrderItem = ({ order }) => (
+    <TableRow className="AdminOrderItem">
+        <TableCell scope="row">{order._id}</TableCell>
+        <TableCell>{order.email ? order.email : '-'}</TableCell>
+        <TableCell>{order.phoneNumber ? order.phoneNumber : '-'}</TableCell>
+        <TableCell>
+            {order.orderGoods
+                ? (order.orderGoods || []).map((orderGood) => (
+                      <Typography variant="body2">
+                          {orderGood.good.name} - {orderGood.count}
+                      </Typography>
+                  ))
+                : '-'}
+        </TableCell>
+        <TableCell>{order.price ? order.price : '-'}</TableCell>
+        <TableCell>{'' + order?.status?.length ? statusNumber[+order.status] : '-'}</TableCell>
+        <TableCell className="edit">
+            <Button component={Link} className="Link" to={`/admin/order/${order._id}/`} variant="contained">
+                Редагувати
+            </Button>
+        </TableCell>
+    </TableRow>
+);
+
+export { AdminOrderItem };

+ 52 - 0
src/components/admin/AdminOrdersPage/AdminOrderList.js

@@ -0,0 +1,52 @@
+import { AdminOrderListHeader } from './AdminOrderListHeader';
+import { connect } from 'react-redux';
+
+import { SearchBar, SearchResults } from '../../common/SearchBar';
+import { actionOrdersFind } from '../../../actions/actionOrdersFind';
+import { actionPromiseClear } from '../../../reducers';
+import { Box, Table, TableBody, TableHead } from '@mui/material';
+import { AdminOrderItem } from './AdminOrderItem';
+import { createSearchParams, useLocation, useNavigate, useSearchParams } from 'react-router-dom';
+
+const CSearchBar = connect(null, {
+    onSearch: (text) => actionOrdersFind({ promiseName: 'adminOrdersFind', text, limit: 5 }),
+    onSearchButtonClick: () => actionPromiseClear('adminOrdersFind'),
+})(SearchBar);
+
+const CSearchResults = connect((state) => ({ items: state.promise.adminOrdersFind?.payload || [] }))(SearchResults);
+
+const AdminOrderList = ({ orders, orderBy = '_id' }) => {
+    const [searchParams, setSearchParams] = useSearchParams();
+
+    return (
+        <Box className="AdminOrderList">
+            <Box className="searchBarWrapper">
+                <CSearchBar
+                    render={CSearchResults}
+                    searchLink="/admin/orders/search/"
+                    renderParams={{ itemLink: '/admin/order/' }}
+                />
+            </Box>
+            <Table>
+                <TableHead>
+                    <AdminOrderListHeader
+                        sort={orderBy}
+                        onSortChange={(orderBy) => {
+                            searchParams.set('orderBy', orderBy);
+                            setSearchParams(searchParams);
+                        }}
+                    />
+                </TableHead>
+                <TableBody>
+                    {(orders || []).map((order) => (
+                        <AdminOrderItem order={order} key={order._id} />
+                    ))}
+                </TableBody>
+            </Table>
+        </Box>
+    );
+};
+
+const CAdminOrderList = connect((state) => ({ orders: state.feed?.payload || [] }))(AdminOrderList);
+
+export { AdminOrderList, CAdminOrderList };

+ 52 - 0
src/components/admin/AdminOrdersPage/AdminOrderListHeader.js

@@ -0,0 +1,52 @@
+import { connect } from 'react-redux';
+
+import { AddButton } from '../../common/AddButton';
+import { TableCell, TableRow, TableSortLabel } from '@mui/material';
+import { useNavigate } from 'react-router-dom';
+
+const AdminOrderListHeader = ({ onSortChange, sort }) => {
+    const navigate = useNavigate();
+    return (
+        <TableRow className="AdminOrderListHeader">
+            <TableCell scope="col">
+                <TableSortLabel
+                    active={sort === '_id' || sort === '-_id'}
+                    direction={sort === '_id' ? 'asc' : 'desc'}
+                    onClick={() => onSortChange(sort === '_id' ? '-_id' : '_id')}
+                >
+                    #
+                </TableSortLabel>
+            </TableCell>
+            <TableCell scope="col">Email</TableCell>
+            <TableCell scope="col">Номер</TableCell>
+            <TableCell scope="col">Товари</TableCell>
+            <TableCell scope="col">
+                <TableSortLabel
+                    active={sort === 'price' || sort === '-price'}
+                    direction={sort === 'price' ? 'asc' : 'desc'}
+                    onClick={() => onSortChange(sort === 'price' ? '-price' : 'price')}
+                >
+                    Ціна
+                </TableSortLabel>
+            </TableCell>
+            <TableCell scope="col">
+                <TableSortLabel
+                    active={sort === 'status' || sort === '-status'}
+                    direction={sort === 'status' ? 'asc' : 'desc'}
+                    onClick={() => onSortChange(sort === 'status' ? '-status' : 'status')}
+                >
+                    Статус
+                </TableSortLabel>
+            </TableCell>
+            <TableCell scope="col">
+                <AddButton
+                    onClick={() => {
+                        navigate('/admin/order/');
+                    }}
+                />
+            </TableCell>
+        </TableRow>
+    );
+};
+
+export { AdminOrderListHeader };

+ 13 - 0
src/components/admin/AdminOrdersPage/index.js

@@ -0,0 +1,13 @@
+import { Box, Typography } from '@mui/material';
+import { CAdminOrderList } from './AdminOrderList';
+
+export const AdminOrdersPage = ({ orderBy }) => {
+    return (
+        <Box className="AdminOrdersPage">
+            <Typography variant="h5" sx={{ marginBottom: '10px', marginTop: '10px' }}>
+                Замовлення
+            </Typography>
+            <CAdminOrderList orderBy={orderBy} />
+        </Box>
+    );
+};

+ 8 - 0
src/components/common/AddButton/index.js

@@ -0,0 +1,8 @@
+import { Button } from '@mui/material';
+import { AiOutlinePlus } from 'react-icons/ai';
+
+export const AddButton = ({ isDisable = false, onClick }) => (
+    <Button onClick={() => onClick()} className="AddButton" disable={isDisable.toString()} variant="contained">
+        Додати
+    </Button>
+);

+ 36 - 0
src/components/common/BuyButton/index.js

@@ -0,0 +1,36 @@
+import { Box, Button } from '@mui/material';
+import { useEffect, useState } from 'react';
+import { connect } from 'react-redux';
+import { actionCartAdd, actionCartDelete } from '../../../reducers';
+
+export const BuyButton = ({ onClick, onDeleteClick, good, cart }) => {
+    const [inCart, setInCart] = useState(false);
+
+    useEffect(() => {
+        setInCart(!!(cart[good._id] && cart[good._id].count) || false);
+    }, [good, cart]);
+    return (
+        <Box className="BuyButton ">
+            {inCart ? (
+                <Button onClick={() => onDeleteClick(good)} variant="outlined">
+                    Вже у кошику
+                </Button>
+            ) : good.amount > 0 ? (
+                <Button onClick={() => onClick(good)} variant="contained" className="button">
+                    Купити
+                </Button>
+            ) : (
+                <Button disabled variant="contained" className="button">
+                    Немає в наявності
+                </Button>
+            )}
+        </Box>
+    );
+};
+
+const CBuyButton = connect((state) => ({ cart: state.cart || {} }), {
+    onClick: (good) => actionCartAdd(good),
+    onDeleteClick: (good) => actionCartDelete(good),
+})(BuyButton);
+
+export { CBuyButton };

+ 14 - 0
src/components/common/Categories/Category.js

@@ -0,0 +1,14 @@
+import { Divider, ListItem, ListItemButton, ListItemText } from '@mui/material';
+import { Fragment } from 'react';
+import { Link } from 'react-router-dom';
+
+export const Category = ({ category = {}, url = '' }) => (
+    <Fragment>
+        <ListItem disablePadding>
+            <ListItemButton component={Link} to={`${url + category._id}`}>
+                <ListItemText primary={category.name || ''} />
+            </ListItemButton>
+        </ListItem>
+        <Divider />
+    </Fragment>
+);

+ 19 - 0
src/components/common/Categories/index.js

@@ -0,0 +1,19 @@
+import { Paper, List, ListItem, ListItemButton, ListItemText, Box } from '@mui/material';
+import { useEffect } from 'react';
+import { Category } from './Category';
+
+const Categories = ({ categories = [], url = '/category/' }) => {
+    return (
+        <Box className="Categories">
+            <List>
+                {(categories || []).map((cat) => (
+                    <Box key={cat._id}>
+                        <Category category={cat} url={url} />
+                    </Box>
+                ))}
+            </List>
+        </Box>
+    );
+};
+
+export { Categories };

+ 66 - 0
src/components/common/DrawerCart/DrawerCart.js

@@ -0,0 +1,66 @@
+import { Link, Navigate, useNavigate } from 'react-router-dom';
+import { actionCartDelete } from '../../../reducers';
+import { IoMdClose } from 'react-icons/io';
+
+import {
+    List,
+    Divider,
+    ListItem,
+    Typography,
+    Button,
+    TableRow,
+    TableBody,
+    Table,
+    TableCell,
+    Stack,
+    ListItemButton,
+    IconButton,
+} from '@mui/material';
+import { useSelector, useDispatch } from 'react-redux';
+import { DrawerCartItem } from './DrawerCartItem';
+import { DrawerRight } from '../DrawerRight';
+import { Box } from '@mui/system';
+
+export const DrawerCart = ({ isOpen = false, onClose = null } = {}) => {
+    const cart = useSelector((state) => state.cart || {});
+    const dispatch = useDispatch();
+    const navigate = useNavigate();
+
+    return (
+        <DrawerRight open={isOpen} onClose={() => onClose()}>
+            <Box className="DrawerCart">
+                <Stack className="list " spacing={2} px={1}>
+                    <Stack spacing={2} direction="row" alignItems="center" justifyContent="space-between" p={1}>
+                        <Typography variant="h5" flexGrow="1">
+                            Кошик
+                        </Typography>
+                        <IconButton onClick={onClose}>
+                            <IoMdClose />
+                        </IconButton>
+                    </Stack>
+
+                    <Divider />
+                    {Object.entries(cart).map(([_id, order]) => (
+                        <DrawerCartItem
+                            order={order}
+                            onDeleteClick={(good) => dispatch(actionCartDelete(good))}
+                            key={_id}
+                        />
+                    ))}
+
+                    {!!Object.keys(cart).length && (
+                        <Button
+                            variant="text"
+                            onClick={() => {
+                                onClose();
+                                navigate('/cart');
+                            }}
+                        >
+                            Підтвердити
+                        </Button>
+                    )}
+                </Stack>
+            </Box>
+        </DrawerRight>
+    );
+};

+ 111 - 0
src/components/common/DrawerCart/DrawerCartItem.js

@@ -0,0 +1,111 @@
+import { Box, width } from '@mui/system';
+import { backendURL } from '../../../helpers';
+import defaultGoodImage from '../../../images/default-good-image.png';
+import { IoCloseOutline } from 'react-icons/io5';
+import { AiOutlinePlus, AiOutlineMinus } from 'react-icons/ai';
+import { actionCartChange } from '../../../reducers';
+import { useEffect, useState } from 'react';
+import { useDispatch } from 'react-redux';
+
+import {
+    ListItem,
+    Grid,
+    Typography,
+    Stack,
+    Container,
+    IconButton,
+    TextField,
+    ButtonGroup,
+    Button,
+    Input,
+    TableCell,
+    TableRow,
+    Card,
+    CardMedia,
+    CardContent,
+} from '@mui/material';
+
+const DrawerCartItem = ({ order, onDeleteClick }) => {
+    const {
+        good: { _id, images = [], name = '', price = 0 },
+    } = order || {};
+
+    return (
+        <Card className="DrawerCartItem">
+            <CardMedia
+                component="img"
+                sx={{ width: 90 }}
+                src={images && images[0]?.url ? `${images ? images[0]?.url : ''}` : defaultGoodImage}
+            />
+            <Box sx={{ display: 'flex', width: '100%' }}>
+                <CardContent className="content">
+                    <Typography component="div" variant="h5">
+                        {name}
+                    </Typography>
+                    <Typography variant="subtitle1" color="text.secondary" component="div">
+                        {price}
+                    </Typography>
+                </CardContent>
+                <Box className="buttons">
+                    <IconButton onClick={() => onDeleteClick({ _id, images, name, price })}>
+                        <IoCloseOutline />
+                    </IconButton>
+                </Box>
+            </Box>
+        </Card>
+    );
+};
+
+export { DrawerCartItem };
+
+{
+    /* <TableRow className="DrawerCartItem">
+<TableCell>
+    <Box
+        component="img"
+        src={images && images[0]?.url ? `/${images ? images[0]?.url : ''}` : defaultGoodImage}
+        sx={{ width: 50 }}
+    />
+</TableCell>
+<TableCell>
+    <Box sx={{ flexGrow: 1 }}>
+        <Typography variant="h5">{name}</Typography>
+        <Typography variant="body1">{price}</Typography>
+    </Box>
+</TableCell>
+<TableCell>
+    <Stack justifyContent="center" direction="row" alignItems="center">
+        <IconButton onClick={() => handleChange(countInput - 1)}>
+            <AiOutlineMinus />
+        </IconButton>
+        <Input
+            value={countInput}
+            onChange={(e) => handleChange(+e.target.value)}
+            size="small"
+            sx={{
+                width: 70,
+                resize: {
+                    fontSize: 10,
+                },
+                px: 3,
+            }}
+        />
+        <IconButton onClick={() => handleChange(countInput + 1)}>
+            <AiOutlinePlus />
+        </IconButton>
+    </Stack>
+</TableCell>
+<TableCell>
+    <Stack justifyContent="center">
+        <Typography variant="body1" textAlign="center">
+            x{count}
+        </Typography>
+    </Stack>
+</TableCell>
+<TableCell>
+    <IconButton onClick={() => onDeleteClick({ _id, images, name, price })}>
+        <IoCloseOutline />
+    </IconButton>
+</TableCell>
+</TableRow> */
+}

+ 11 - 0
src/components/common/DrawerRight/index.js

@@ -0,0 +1,11 @@
+import { Drawer } from '@mui/material';
+import ReactDOM from 'react-dom';
+
+export const DrawerRight = ({ children, onClose = null, open } = {}) => {
+    return ReactDOM.createPortal(
+        <Drawer anchor="right" className="DrawerRight" open={open} onClose={onClose}>
+            {children}
+        </Drawer>,
+        document.body
+    );
+};

+ 20 - 0
src/components/common/DropZone/index.js

@@ -0,0 +1,20 @@
+import { useDropzone } from 'react-dropzone';
+import { useEffect } from 'react';
+import { connect } from 'react-redux';
+import { Box } from '@mui/material';
+export const DropZone = ({ onFileDrop, children }) => {
+    const { acceptedFiles, getRootProps, getInputProps } = useDropzone();
+
+    useEffect(() => {
+        if (acceptedFiles.length) {
+            onFileDrop(acceptedFiles);
+        }
+    }, [acceptedFiles]);
+
+    return (
+        <Box {...getRootProps({ className: 'Dropzone' })}>
+            {/* <input {...getInputProps()} /> */}
+            {children}
+        </Box>
+    );
+};

+ 2 - 0
src/components/common/EntityEditor/SortableItem.js

@@ -0,0 +1,2 @@
+import { SortableElement } from 'react-sortable-hoc';
+export const SortableItem = SortableElement(({ children }) => <div className="SortableItem">{children}</div>);

+ 5 - 0
src/components/common/EntityEditor/SortableList.js

@@ -0,0 +1,5 @@
+import { SortableContainer } from 'react-sortable-hoc';
+
+export const SortableList = SortableContainer(({ children }) => {
+    return <div className="SortableContainer">{children}</div>;
+});

+ 81 - 0
src/components/common/EntityEditor/index.js

@@ -0,0 +1,81 @@
+import { useEffect, useState } from 'react';
+import { arrayMoveImmutable } from 'array-move';
+import { DropZone } from '../DropZone';
+import { SortableList } from './SortableList';
+import { SortableItem } from './SortableItem';
+import { Box, Button, IconButton, ImageList, ImageListItem, ImageListItemBar, Typography } from '@mui/material';
+import { MdClose } from 'react-icons/md';
+
+export const EntityEditor = ({ entity = { images: [] }, onSave, onFileDrop, uploadFiles, onImagesSave }) => {
+    const [state, setState] = useState(entity);
+
+    useEffect(() => {
+        setState(entity);
+        console.log(entity.images);
+    }, [entity]);
+
+    useEffect(() => {
+        if (uploadFiles?.status === 'FULFILLED') {
+            setState({ ...state, images: [...(state.images || []), ...uploadFiles?.payload] });
+        }
+    }, [uploadFiles]);
+
+    useEffect(() => {
+        onImagesSave && onImagesSave(state.images?.filter((img) => img?._id && img?.url));
+    }, [state]);
+
+    const onSortEnd = ({ oldIndex, newIndex }) => {
+        console.log(arrayMoveImmutable(state.images, oldIndex, newIndex));
+        setState({ ...state, images: arrayMoveImmutable(state.images, oldIndex, newIndex) });
+    };
+    const onItemRemove = (toRemoveId) => {
+        setState({ ...state, images: [...state.images.filter((el) => el?._id !== toRemoveId)] });
+    };
+
+    return (
+        <Box className="EntityEditor">
+            <DropZone onFileDrop={(files) => onFileDrop(files)}>
+                <Typography>Drop images here!</Typography>
+            </DropZone>
+            <SortableList pressDelay={200} onSortEnd={onSortEnd} axis="xy" className="SortableContainer">
+                <ImageList sx={{ maxHeight: 800 }} cols={3} fullwidth>
+                    {state.images?.map(
+                        (image, index) =>
+                            !!image?._id &&
+                            !!image?.url && (
+                                <SortableItem key={`item-${image._id}`} index={index}>
+                                    <ImageListItem key={image._id}>
+                                        <ImageListItemBar
+                                            sx={{
+                                                background: 'rgba(0,0,0,0.1)',
+                                            }}
+                                            actionIcon={
+                                                <IconButton onClick={() => onItemRemove(image._id)}>
+                                                    <MdClose />
+                                                </IconButton>
+                                            }
+                                        />
+                                        <Box
+                                            component="img"
+                                            className="DropZoneImage"
+                                            src={`${image.url}`}
+                                            loading="lazy"
+                                        />
+                                    </ImageListItem>
+                                </SortableItem>
+                            )
+                    )}
+                </ImageList>
+            </SortableList>
+            {!!onSave && (
+                <Button
+                    onClick={() => {
+                        onSave(entity._id, state.images);
+                    }}
+                >
+                    Save
+                </Button>
+            )}
+        </Box>
+    );
+};

+ 9 - 0
src/components/common/Error/index.js

@@ -0,0 +1,9 @@
+const { Typography } = require('@mui/material');
+
+const Error = ({ children }) => (
+    <Typography variant="body1" sx={{ textAlign: 'center' }} className="error">
+        {children}
+    </Typography>
+);
+
+export { Error };

+ 4 - 0
src/components/common/Error404/index.js

@@ -0,0 +1,4 @@
+import { Box } from '@mui/material';
+import { ReactComponent as Logo404 } from '../../../images/404.svg';
+
+export const Error404 = () => <Box className="Error404" component={Logo404} />;

+ 37 - 0
src/components/common/GoodCard/index.js

@@ -0,0 +1,37 @@
+import { Button, Card, CardActionArea, CardActions, CardContent, CardMedia, Typography } from '@mui/material';
+import { connect } from 'react-redux';
+import { Link } from 'react-router-dom';
+import defaultGoodImage from '../../../images/default-good-image.png';
+import { actionCartAdd } from '../../../reducers';
+import { CBuyButton } from '../BuyButton';
+
+const GoodCard = ({ good = {} }) => {
+    return (
+        <Card className="GoodCard">
+            <CardActionArea component={Link} to={`/good/${good._id}`}>
+                <CardMedia
+                    component="img"
+                    height="200"
+                    image={`${good.images ? good.images[0]?.url : defaultGoodImage}`}
+                />
+                <CardContent>
+                    <Typography gutterBottom variant="body1" component="div" color="#1C1B1F" textAlign="left">
+                        Назва: {good.name?.length > 10 ? `${good.name.slice(0, 10)}...` : good.name}
+                    </Typography>
+                    <Typography variant="body2" color="text.secondary" textAlign="left">
+                        Ціна: {good.price}
+                    </Typography>
+                </CardContent>
+            </CardActionArea>
+            <CardActions>
+                <CBuyButton good={good} key={good._id} />
+            </CardActions>
+        </Card>
+    );
+};
+
+const CGoodCard = connect(null, {
+    handleOnClick: (good) => actionCartAdd(good),
+})(GoodCard);
+
+export { GoodCard, CGoodCard };

+ 14 - 0
src/components/common/GoodCardSlider/GoodCardSet.js

@@ -0,0 +1,14 @@
+import { Box, Stack } from '@mui/material';
+import { CGoodCard } from '../GoodCard';
+
+export const GoodCardSet = ({ goods = [], num = 4 } = {}) => {
+    return (
+        <Stack className="GoodCardSet" direction="row">
+            {(goods || []).map((good) => (
+                <Box sx={{ width: `${Math.floor(100 / (num || 1))}%`, padding: '10px' }} key={good?._id}>
+                    <CGoodCard good={good} buyButton={false} />
+                </Box>
+            ))}
+        </Stack>
+    );
+};

+ 37 - 0
src/components/common/GoodCardSlider/GoodCardSlider.js

@@ -0,0 +1,37 @@
+import { Carousel } from 'react-responsive-carousel';
+import { GoodCardSet } from './GoodCardSet';
+import { useState, useEffect } from 'react';
+import { Box } from '@mui/material';
+export const GoodCardSlider = ({ goods = [] } = {}) => {
+    const [goodSets, setGoodSets] = useState([]);
+    const num = 5;
+
+    useEffect(() => {
+        if (goods?.length) {
+            let goodSets = [];
+            for (let i = 0; i < goods.slice(0, 20).length; i += num) {
+                if (i + num > goods.length && goods.length % num !== 0) {
+                    goodSets.push(goods.slice(i, goods.length));
+                    break;
+                }
+                goodSets.push(goods.slice(i, i + num));
+            }
+            setGoodSets(goodSets);
+        }
+    }, [goods]);
+
+    return (
+        <Box className="GoodCardSlider">
+            <Carousel className="Slider" showThumbs={false} showStatus={false} showIndicators={false}>
+                {(goodSets || []).map((goodSet, idx) => (
+                    <Box key={idx}>
+                        <GoodCardSet goods={goodSet} num={num} />
+                    </Box>
+                ))}
+            </Carousel>
+        </Box>
+    );
+};
+// autoPlay={true}
+// autoPlaySpeed={1000}
+// transitionDuration={500}

+ 3 - 0
src/components/common/GoodCardSlider/index.js

@@ -0,0 +1,3 @@
+import { GoodCardSlider } from "./GoodCardSlider";
+
+export { GoodCardSlider };

+ 16 - 0
src/components/common/GoodList/index.js

@@ -0,0 +1,16 @@
+import { Box, Grid } from '@mui/material';
+import { GoodCard } from '../GoodCard';
+
+export const GoodList = ({ goods = [] } = {}) => {
+    return (
+        <Box className="GoodList">
+            <Grid container spacing={2}>
+                {(goods || []).map((good) => (
+                    <Grid item xs={3} key={good._id}>
+                        <GoodCard good={good} />
+                    </Grid>
+                ))}
+            </Grid>
+        </Box>
+    );
+};

+ 14 - 0
src/components/common/ProtectedRoute/index.js

@@ -0,0 +1,14 @@
+import { Navigate, Route, useLocation } from 'react-router-dom';
+import { connect } from 'react-redux';
+
+export const ProtectedRoute = ({ roles = ['anon'], children, fallback = '/', auth } = {}) => {
+    let location = useLocation();
+    !!auth.length || (auth = ['anon']);
+    if (!auth.filter((role) => roles.includes(role)).length) {
+        return <Navigate to={fallback} state={{ from: location }} />;
+    }
+
+    return children;
+};
+
+export const CProtectedRoute = connect((state) => ({ auth: state.auth?.payload?.sub?.acl || [] }))(ProtectedRoute);

+ 109 - 0
src/components/common/SearchBar/SearchBar.js

@@ -0,0 +1,109 @@
+import { useEffect, useState, useRef } from 'react';
+import { connect, useDispatch } from 'react-redux';
+import { Link } from 'react-router-dom';
+import { AiOutlineCloseCircle } from 'react-icons/ai';
+
+import { Box, TextField, Button, Container, Stack, IconButton } from '@mui/material';
+import { actionGoodsFind } from '../../../actions/actionGoodsFind';
+import { actionPromiseClear } from '../../../reducers';
+
+// import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+// import { faSearch } from "@fortawesome/free-solid-svg-icons";
+
+export const SearchBar = ({
+    onSearch,
+    onSearchButtonClick,
+    render = null,
+    renderParams = {},
+    searchLink = '/search/',
+} = {}) => {
+    const ref = useRef();
+    const [inputValue, setInputValue] = useState('');
+    const [isChildrenOpen, setIsChildrenOpen] = useState(false);
+    const [inputTimeout, setInputTimeout] = useState(null);
+    const R = render;
+
+    useEffect(() => {
+        if (inputTimeout) {
+            clearTimeout(inputTimeout);
+        }
+
+        const checkClickOutsideHeaderSearchBar = (e) => {
+            if (ref.current && !ref.current.contains(e.target)) {
+                setIsChildrenOpen(false);
+            } else {
+                inputValue.length && setIsChildrenOpen(true);
+            }
+        };
+        if (inputTimeout) {
+            clearTimeout(inputTimeout);
+            setInputTimeout(null);
+        }
+        setInputTimeout(
+            setTimeout(() => {
+                inputValue && onSearch(inputValue);
+            }, 700)
+        );
+
+        setIsChildrenOpen(!!inputValue?.length);
+        document.addEventListener('mousedown', checkClickOutsideHeaderSearchBar);
+        return () => {
+            document.removeEventListener('mousedown', checkClickOutsideHeaderSearchBar);
+        };
+    }, [inputValue]);
+
+    return (
+        <Box className={`SearchBar ${!isChildrenOpen && 'hide'}`} ref={ref}>
+            <Stack direction="row" alignItems="center">
+                <TextField
+                    variant="standard"
+                    value={inputValue}
+                    placeholder="Пошук"
+                    onChange={(e) => setInputValue(e.target.value)}
+                    className="SearchBarInput"
+                    InputProps={{
+                        endAdornment: (
+                            <IconButton onClick={() => setInputValue('')} edge="end">
+                                {inputValue && <AiOutlineCloseCircle />}
+                            </IconButton>
+                        ),
+                    }}
+                />
+
+                {!!inputValue ? (
+                    <Button
+                        className="Link"
+                        onClick={() => {
+                            setInputValue('');
+                            onSearchButtonClick();
+                        }}
+                        variant="text"
+                        color="inherit"
+                    >
+                        Пошук
+                    </Button>
+                ) : (
+                    <Button variant="text" color="inherit">
+                        Пошук
+                    </Button>
+                )}
+            </Stack>
+            <Stack direction="row">
+                {isChildrenOpen && (
+                    <R
+                        onItemClick={() => {
+                            setInputValue('');
+                            onSearchButtonClick();
+                        }}
+                        {...renderParams}
+                    />
+                )}
+            </Stack>
+        </Box>
+    );
+};
+
+export const CSearchBar = connect(null, {
+    onSearch: (text) => actionGoodsFind({ text, limit: 5 }),
+    onSearchButtonClick: () => actionPromiseClear('goodsFind'),
+})(SearchBar);

+ 17 - 0
src/components/common/SearchBar/SearchCategoryResultItem.js

@@ -0,0 +1,17 @@
+import { Link } from 'react-router-dom';
+import { Stack, Typography } from '@mui/material';
+const SearchCategoryResultItem = ({ category, onClick, link = '' } = {}) => {
+    const { _id = null, name = '' } = category || {};
+
+    return (
+        <Link className="Link" to={`${link}${_id}/`}>
+            <Stack direction="row" className="SearchCategoryResultItem" onClick={() => onClick && onClick()}>
+                <Typography sx={{ flexGrow: 1 }}>{name.length > 30 ? `${name.substring(0, 30)}...` : name}</Typography>
+
+                <Typography>{_id}</Typography>
+            </Stack>
+        </Link>
+    );
+};
+
+export default SearchCategoryResultItem;

+ 37 - 0
src/components/common/SearchBar/SearchGoodResultItem.js

@@ -0,0 +1,37 @@
+import { Link } from 'react-router-dom';
+import defaultGoodImage from '../../../images/default-good-image.png';
+
+import { Grid, Box, Stack, Typography } from '@mui/material';
+const SearchGoodResultItem = ({ good, onClick, link = '' } = {}) => {
+    const { _id = 0, images = [], name = '', description = '', price = '' } = good || {};
+
+    return (
+        <Grid
+            container
+            component={Link}
+            to={`${link}${_id}/`}
+            className="SearchGoodResultItem Link"
+            onClick={() => onClick && onClick()}
+            spacing={1}
+        >
+            <Grid item xs={3}>
+                <Box component="img" src={images ? images[0]?.url : defaultGoodImage} />
+            </Grid>
+            <Grid item xs={6}>
+                <Box sx={{ p: 1 }}>
+                    <Typography variant="body1" sx={{ flexGrow: 1 }}>
+                        {name.length > 30 ? `${name.substring(0, 30)}...` : name}
+                    </Typography>
+                    <Typography variant="body2">
+                        {description.length > 70 ? `${description.substring(0, 70)}...` : description}
+                    </Typography>
+                </Box>
+            </Grid>
+            <Grid item xs={3} sx={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end' }}>
+                <Typography variant="body1">{price}</Typography>
+            </Grid>
+        </Grid>
+    );
+};
+
+export default SearchGoodResultItem;

+ 23 - 0
src/components/common/SearchBar/SearchOrderResultItem.js

@@ -0,0 +1,23 @@
+import { Link } from 'react-router-dom';
+import { Box, Grid, Stack, Typography } from '@mui/material';
+const SearchOrderResultItem = ({ order, onClick, link = '' } = {}) => {
+    const { _id = null, email = '', phoneNumber = '' } = order || {};
+
+    return (
+        <Stack
+            component={Link}
+            to={`${link}${_id}/`}
+            className="SearchOrderResultItem Link"
+            onClick={() => onClick && onClick()}
+            spacing={1}
+        >
+            <Typography variant="body1">ID:{_id}</Typography>
+
+            <Typography>Email:{email.length > 30 ? `${email.substring(0, 30)}...` : email}</Typography>
+
+            <Typography>Номер:{phoneNumber}</Typography>
+        </Stack>
+    );
+};
+
+export default SearchOrderResultItem;

+ 71 - 0
src/components/common/SearchBar/SearchResults.js

@@ -0,0 +1,71 @@
+import { connect } from 'react-redux';
+import { useEffect } from 'react';
+import SearchGoodResultItem from './SearchGoodResultItem';
+import { HiOutlineEmojiSad } from 'react-icons/hi';
+import SearchCategoryResultItem from './SearchCategoryResultItem';
+import { Divider, Paper, Stack, Typography } from '@mui/material';
+import { Box } from '@mui/system';
+import { Error } from '../Error';
+import SearchOrderResultItem from './SearchOrderResultItem';
+
+export const SearchResults = ({ items, onItemClick, itemLink = '' }) => {
+    useEffect(() => {
+        console.log(items);
+    }, [items]);
+    useEffect(() => {
+        console.log(itemLink.match(/.+(order || orders).+/));
+    }, [itemLink]);
+    return (
+        <Paper className="SearchResults">
+            <Stack>
+                {!!items?.length ? (
+                    itemLink.match(/.+(good).+/) ? (
+                        items.map((good) => (
+                            <Box>
+                                <SearchGoodResultItem
+                                    link={itemLink}
+                                    good={good}
+                                    key={good._id}
+                                    onClick={() => onItemClick && onItemClick()}
+                                />
+                                <Divider sx={{ my: 1 }} />
+                            </Box>
+                        ))
+                    ) : itemLink.match(/.+(category|categories).+/) ? (
+                        items.map((cat) => (
+                            <Box>
+                                <SearchCategoryResultItem
+                                    link={itemLink}
+                                    category={cat}
+                                    key={cat._id}
+                                    onClick={() => onItemClick && onItemClick()}
+                                />
+                                <Divider sx={{ my: 1 }} />
+                            </Box>
+                        ))
+                    ) : itemLink.match(/.+(order|orders).+/) ? (
+                        items.map((order) => (
+                            <Box>
+                                <SearchOrderResultItem
+                                    link={itemLink}
+                                    order={order}
+                                    key={order._id}
+                                    onClick={() => onItemClick && onItemClick()}
+                                />
+                                <Divider sx={{ my: 1 }} />
+                            </Box>
+                        ))
+                    ) : (
+                        []
+                    )
+                ) : (
+                    <Error>Нічого не знайдено</Error>
+                )}
+            </Stack>
+        </Paper>
+    );
+};
+
+const CSearchResults = connect((state) => ({ items: state.promise.goodsFind?.payload || [] }))(SearchResults);
+
+export { CSearchResults };

+ 7 - 0
src/components/common/SearchBar/index.js

@@ -0,0 +1,7 @@
+import { SearchBar, CSearchBar } from './SearchBar';
+
+import { SearchResults, CSearchResults } from './SearchResults';
+// import SearchGoodResultItem from './SearchGoodResultItem';
+
+// export { SearchBar, SearchResults, CSearchResults, SearchGoodResultItem };
+export { SearchBar, CSearchBar, SearchResults, CSearchResults };

+ 60 - 0
src/components/common/SortOptions/index.js

@@ -0,0 +1,60 @@
+import { Box, Button, Menu, MenuItem } from '@mui/material';
+import { useEffect, useState } from 'react';
+import { sortOptions } from '../../../helpers/sortOptions';
+
+export const SortOptions = ({ onClick, options = sortOptions || [] } = {}) => {
+    const [anchorEl, setAnchorEl] = useState(null);
+    const [selectedOption, setSelectedOption] = useState(options[0] || null);
+    const open = Boolean(anchorEl);
+    const handleClick = (event) => {
+        setAnchorEl(event.currentTarget);
+    };
+
+    const handleSelect = (option) => {
+        option && setSelectedOption(option);
+        console.log(option);
+        setAnchorEl(null);
+    };
+
+    useEffect(() => {
+        if (selectedOption) {
+            console.log(selectedOption);
+            onClick(selectedOption);
+        }
+    }, [selectedOption]);
+
+    return (
+        <Box className="SortOptions">
+            <Button
+                id="demo-positioned-button"
+                aria-controls={open ? 'demo-positioned-menu' : undefined}
+                aria-haspopup="true"
+                aria-expanded={open ? 'true' : undefined}
+                onClick={handleClick}
+            >
+                {selectedOption.label}
+            </Button>
+            <Menu
+                id="demo-positioned-menu"
+                aria-labelledby="demo-positioned-button"
+                anchorEl={anchorEl}
+                open={open}
+                onClose={() => setAnchorEl(null)}
+                anchorOrigin={{
+                    vertical: 'top',
+                    horizontal: 'left',
+                }}
+                transformOrigin={{
+                    vertical: 'top',
+                    horizontal: 'left',
+                }}
+            >
+                {(options || []).map((option) => (
+                    <MenuItem key={option.value} onClick={(e) => handleSelect(option)}>
+                        {option.label}
+                    </MenuItem>
+                ))}
+            </Menu>
+        </Box>
+    );
+};

+ 18 - 0
src/components/layout/Aside/AdminCategories.js

@@ -0,0 +1,18 @@
+import { Categories } from '../../common/Categories';
+
+const adminCategories = [
+    {
+        _id: 'goods/',
+        name: 'Товари',
+    },
+    {
+        _id: 'categories/',
+        name: 'Категорії',
+    },
+    {
+        _id: 'orders/',
+        name: 'Замовлення',
+    },
+];
+
+export const AdminCategories = () => <Categories categories={adminCategories} url="/admin/" />;

+ 8 - 0
src/components/layout/Aside/CCategories.js

@@ -0,0 +1,8 @@
+import { connect } from 'react-redux';
+import { Categories } from '../../common/Categories';
+
+const CCategories = connect((state) => ({
+    categories: state.promise.rootCats?.payload || [],
+}))(Categories);
+
+export { CCategories };

+ 28 - 0
src/components/layout/Aside/StatusOptions.js

@@ -0,0 +1,28 @@
+import { Box, Divider, List, ListItem, ListItemButton, ListItemText } from '@mui/material';
+import { createSearchParams, useLocation, useNavigate, useSearchParams } from 'react-router-dom';
+
+export const StatusOptions = ({ options }) => {
+    const [searchParams, setSearchParams] = useSearchParams();
+
+    return (
+        <Box className="StatusOptions">
+            <List>
+                {[{ value: 0, label: 'Всі' }, ...(options || [])].map((option) => (
+                    <Box key={option.value}>
+                        <ListItem disablePadding>
+                            <ListItemButton
+                                onClick={() => {
+                                    searchParams.set('status', option.value);
+                                    setSearchParams(searchParams);
+                                }}
+                            >
+                                <ListItemText primary={option.label || ''} />
+                            </ListItemButton>
+                        </ListItem>
+                        <Divider />
+                    </Box>
+                ))}
+            </List>
+        </Box>
+    );
+};

+ 46 - 0
src/components/layout/Aside/index.js

@@ -0,0 +1,46 @@
+import { Box } from '@mui/material';
+import { Navigate, Route, Routes } from 'react-router-dom';
+import { statusOptions } from '../../../helpers';
+import { CProtectedRoute } from '../../common/ProtectedRoute';
+import { AdminCategories } from './AdminCategories';
+
+import { CCategories } from './CCategories';
+import { StatusOptions } from './StatusOptions';
+
+const Aside = ({ children }) => (
+    <Box className="Aside">
+        <Box className="body">
+            <Routes>
+                <Route
+                    path="/admin/*"
+                    exact
+                    element={
+                        <CProtectedRoute roles={['admin']} fallback="/auth">
+                            <AdminCategories />
+                        </CProtectedRoute>
+                    }
+                />
+                <Route path="/*" element={<CCategories />} />
+                <Route path="*" element={<Navigate to="/404" />} />
+            </Routes>
+
+            {children}
+        </Box>
+
+        <Routes>
+            <Route
+                path="/admin/orders"
+                exact
+                element={
+                    <Box className="body" mt={4}>
+                        <CProtectedRoute roles={['admin']} fallback="/auth">
+                            <StatusOptions options={statusOptions} />
+                        </CProtectedRoute>
+                    </Box>
+                }
+            />
+        </Routes>
+    </Box>
+);
+
+export { Aside };

+ 5 - 0
src/components/layout/Content/index.js

@@ -0,0 +1,5 @@
+import { Paper, Box } from '@mui/material';
+
+const Content = ({ children }) => <Box className="Content">{children}</Box>;
+
+export default Content;

+ 94 - 0
src/components/layout/Footer/index.js

@@ -0,0 +1,94 @@
+import { Container, Grid, Table, TableBody, TableCell, TableRow, Typography } from '@mui/material';
+import { Box } from '@mui/system';
+
+const Footer = () => (
+    <Box className="Footer">
+        <Container>
+            <Grid container>
+                <Grid item xs={2}></Grid>
+                <Grid item xs={2}>
+                    <Typography variant="body1" textAlign="left">
+                        Графік роботи
+                    </Typography>
+                    <Table>
+                        <TableBody>
+                            <TableRow>
+                                <TableCell className="TableCell">
+                                    <Typography variant="body2" textAlign="left">
+                                        Пн-Пт:
+                                    </Typography>
+                                </TableCell>
+                                <TableCell className="TableCell">
+                                    <Typography variant="body2" textAlign="left">
+                                        9.00-18.00
+                                    </Typography>
+                                </TableCell>
+                            </TableRow>
+                            <TableRow>
+                                <TableCell className="TableCell">
+                                    <Typography variant="body2" textAlign="left">
+                                        Субота:
+                                    </Typography>
+                                </TableCell>
+                                <TableCell className="TableCell">
+                                    <Typography variant="body2" textAlign="left">
+                                        9.00-15.00
+                                    </Typography>
+                                </TableCell>
+                            </TableRow>
+                            <TableRow>
+                                <TableCell className="TableCell">
+                                    <Typography variant="body2" textAlign="left">
+                                        Неділя
+                                    </Typography>
+                                </TableCell>
+                                <TableCell className="TableCell">
+                                    <Typography variant="body2" textAlign="left">
+                                        вихідний
+                                    </Typography>
+                                </TableCell>
+                            </TableRow>
+                        </TableBody>
+                    </Table>
+                </Grid>
+                <Grid item xs={3}></Grid>
+                <Grid item xs={3}>
+                    <Typography variant="body1" textAlign="left">
+                        Контакти
+                    </Typography>
+                    <Table>
+                        <TableBody>
+                            <TableRow>
+                                <TableCell className="TableCell">
+                                    <Typography variant="body2" textAlign="left">
+                                        Тел:
+                                    </Typography>
+                                </TableCell>
+                                <TableCell className="TableCell">
+                                    <Typography variant="body2" textAlign="left">
+                                        +380667213260
+                                    </Typography>
+                                </TableCell>
+                            </TableRow>
+                            <TableRow>
+                                <TableCell className="TableCell">
+                                    <Typography variant="body2" textAlign="left">
+                                        Email:
+                                    </Typography>
+                                </TableCell>
+                                <TableCell className="TableCell">
+                                    <Typography variant="body2" textAlign="left">
+                                        illya.shyyan@hneu.net
+                                    </Typography>
+                                </TableCell>
+                            </TableRow>
+                        </TableBody>
+                    </Table>
+                </Grid>
+                <Grid item xs={2}></Grid>
+            </Grid>
+        </Container>
+    </Box>
+);
+
+export { Footer };

+ 24 - 0
src/components/layout/Header/CartIcon/index.js

@@ -0,0 +1,24 @@
+import { Badge, Box, IconButton } from '@mui/material';
+import { useEffect, useState } from 'react';
+import { connect } from 'react-redux';
+import { MdOutlineShoppingCart } from 'react-icons/md';
+
+export const CartIcon = ({ cart }) => {
+    const [count, setCount] = useState(0);
+    useEffect(() => {
+        let count = 0;
+        for (let order of Object.values(cart)) {
+            count += +order.count;
+        }
+        setCount(count);
+    }, [cart]);
+    return (
+        <Box className="CartIcon">
+            <Badge badgeContent={count} color="primary">
+                <MdOutlineShoppingCart className="CartLogo" />
+            </Badge>
+        </Box>
+    );
+};
+
+export const CCartIcon = connect((state) => ({ cart: state.cart || {} }))(CartIcon);

+ 19 - 0
src/components/layout/Header/LogoutIcon/index.js

@@ -0,0 +1,19 @@
+import { Badge, Box, IconButton } from '@mui/material';
+import { useEffect, useState } from 'react';
+import { connect, useDispatch, useSelector } from 'react-redux';
+import { MdLogout, MdOutlineShoppingCart } from 'react-icons/md';
+import { useSelect } from '@mui/base';
+import { actionLogout } from '../../../../actions/actionLogout';
+
+export const LogoutIcon = () => {
+    const dispatch = useDispatch();
+    const token = useSelector((state) => state.auth?.token || null);
+
+    return token ? (
+        <Box className="LogoutIcon">
+            <IconButton onClick={() => dispatch(actionLogout())}>
+                <MdLogout className="LogoutLogo" />
+            </IconButton>
+        </Box>
+    ) : null;
+};

+ 56 - 0
src/components/layout/Header/index.js

@@ -0,0 +1,56 @@
+import { AppBar, Box, Button, IconButton, Stack, TextField, Toolbar, Typography } from '@mui/material';
+import { useState } from 'react';
+
+import { useSelector } from 'react-redux';
+import { Link } from 'react-router-dom';
+import { ReactComponent as ShoppingLogo } from '../../../images/shopping-logo.svg';
+import { DrawerCart } from '../../common/DrawerCart/DrawerCart';
+import { CSearchBar, SearchBar } from '../../common/SearchBar';
+import { CSearchResults } from '../../common/SearchBar/SearchResults';
+import { CCartIcon } from './CartIcon';
+import { LogoutIcon } from './LogoutIcon';
+
+const Header = () => {
+    const rootCats = useSelector((state) => state?.promise?.rootCats?.payload || []);
+    const [isCartDrawerOpen, setIsCartDrawerOpen] = useState(false);
+    return (
+        <Box className="Header">
+            <AppBar position="static" className="AppBar">
+                <Toolbar variant="dense" className="ToolBar">
+                    <IconButton component={Link} to="/">
+                        <ShoppingLogo className="Logo" />
+                    </IconButton>
+                    <Stack direction="row" spacing={2}>
+                        <Button variant="text" color="inherit" component={Link} to="/">
+                            <Typography variant="body1" component="div">
+                                Головна
+                            </Typography>
+                        </Button>
+                        <Button
+                            variant="text"
+                            color="inherit"
+                            component={Link}
+                            to={rootCats[0] ? `/category/${rootCats[0]._id}` : '/'}
+                        >
+                            <Typography variant="body1" component="div">
+                                Товари
+                            </Typography>
+                        </Button>
+                    </Stack>
+                    <Box className="SearchBarWrapper">
+                        <CSearchBar render={CSearchResults} renderParams={{ itemLink: '/good/' }} />
+                    </Box>
+                    <LogoutIcon />
+                    <IconButton color="inherit" className="CartLogoButton" onClick={() => setIsCartDrawerOpen(true)}>
+                        <Box>
+                            <CCartIcon />
+                        </Box>
+                    </IconButton>
+                </Toolbar>
+            </AppBar>
+            <DrawerCart isOpen={isCartDrawerOpen} onClose={() => setIsCartDrawerOpen(false)} />
+        </Box>
+    );
+};
+
+export { Header };

+ 1 - 0
src/helpers/delay.js

@@ -0,0 +1 @@
+export const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

+ 1 - 0
src/helpers/getQuery.js

@@ -0,0 +1 @@
+

+ 6 - 0
src/helpers/index.js

@@ -0,0 +1,6 @@
+import { jwtDecode } from './jwtDecode';
+import { mock } from './mock';
+import { delay } from './delay';
+import { statusNumber, statusOptions } from './orderStatus';
+
+export { jwtDecode, mock, delay, statusNumber, statusOptions };

+ 9 - 0
src/helpers/jwtDecode.js

@@ -0,0 +1,9 @@
+export const jwtDecode = (token) => {
+    try {
+        let payload = JSON.parse(atob(token.split('.')[1]));
+
+        return payload;
+    } catch (e) {
+        console.log(e);
+    }
+};

+ 0 - 0
src/helpers/mock.js


+ 25 - 0
src/helpers/orderStatus.js

@@ -0,0 +1,25 @@
+export const statusNumber = {
+    1: 'В обробці',
+    2: 'Підтверджено',
+    3: 'Виконано',
+    4: 'Скасовано',
+};
+
+export const statusOptions = [
+    {
+        value: 1,
+        label: statusNumber[1],
+    },
+    {
+        value: 2,
+        label: statusNumber[2],
+    },
+    {
+        value: 3,
+        label: statusNumber[3],
+    },
+    {
+        value: 4,
+        label: statusNumber[4],
+    },
+];

+ 0 - 0
src/helpers/sortOptions.js


Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels