gennadysht 1 anno fa
commit
8904eb4598
77 ha cambiato i file con 25277 aggiunte e 0 eliminazioni
  1. 23 0
      .gitignore
  2. 5 0
      .vscode/extensions.json
  3. 23 0
      .vscode/launch.json
  4. 70 0
      README.md
  5. 20833 0
      package-lock.json
  6. 69 0
      package.json
  7. BIN
      public/favicon.ico
  8. 43 0
      public/index.html
  9. BIN
      public/logo192.png
  10. BIN
      public/logo512.png
  11. 25 0
      public/manifest.json
  12. 3 0
      public/robots.txt
  13. BIN
      public/thememain.png
  14. 38 0
      src/App.css
  15. 70 0
      src/App.js
  16. 27 0
      src/Components/AvatarAnimated.js
  17. 58 0
      src/Components/Cart.js
  18. 78 0
      src/Components/CartGood.js
  19. 61 0
      src/Components/CartGoodsList.js
  20. 94 0
      src/Components/Category.js
  21. 35 0
      src/Components/CategoryBreadcrumbs.js
  22. 135 0
      src/Components/CategoryList.js
  23. 151 0
      src/Components/CategoryTree.js
  24. 15 0
      src/Components/CategoryTree.module.css
  25. 71 0
      src/Components/CategoryTreeItem.js
  26. 30 0
      src/Components/CategoryTreeItem.module.css
  27. 143 0
      src/Components/DropDownList.js
  28. 120 0
      src/Components/EditableCategory.js
  29. 180 0
      src/Components/EditableGood.js
  30. 142 0
      src/Components/EditableUser.js
  31. 32 0
      src/Components/FileDropZone.js
  32. 160 0
      src/Components/Good.js
  33. 90 0
      src/Components/GoodItem.js
  34. 43 0
      src/Components/GoodsList.js
  35. 9 0
      src/Components/LackPermissions.js
  36. 5 0
      src/Components/LoadingState.js
  37. 87 0
      src/Components/LoginForm.js
  38. 17 0
      src/Components/Logout.js
  39. 85 0
      src/Components/MainAppBar.js
  40. 43 0
      src/Components/ModalContainer.js
  41. 12 0
      src/Components/MyLink.js
  42. 56 0
      src/Components/Order.js
  43. 55 0
      src/Components/OrderGood.js
  44. 61 0
      src/Components/OrderGoodsList.js
  45. 138 0
      src/Components/OrderList.js
  46. 42 0
      src/Components/Pagination.js
  47. 37 0
      src/Components/ReferenceLink.js
  48. 109 0
      src/Components/RegisterForm.js
  49. 64 0
      src/Components/RootCats.js
  50. 90 0
      src/Components/SearchInput.js
  51. 65 0
      src/Components/Sidebar.js
  52. 131 0
      src/Components/SortedFileDropZone.js
  53. 23 0
      src/Components/StyledTableElements.js
  54. 131 0
      src/Components/UsersList.js
  55. 4 0
      src/Components/cartGood.css
  56. 21 0
      src/Components/index.js
  57. 4 0
      src/Components/orderGood.css
  58. 84 0
      src/Entities/UserEntity.js
  59. 1 0
      src/Entities/index.js
  60. BIN
      src/images/logo.jpg
  61. BIN
      src/images/theme_main.png
  62. 14 0
      src/index.css
  63. 15 0
      src/index.js
  64. 1 0
      src/logo.svg
  65. 160 0
      src/reducers/authReducer.js
  66. 116 0
      src/reducers/cartReducer.js
  67. 147 0
      src/reducers/categoryReducer.js
  68. 127 0
      src/reducers/frontEndReducer.js
  69. 132 0
      src/reducers/goodsReducer.js
  70. 9 0
      src/reducers/index.js
  71. 141 0
      src/reducers/ordersReducer.js
  72. 13 0
      src/reportWebVitals.js
  73. 5 0
      src/setupTests.js
  74. 54 0
      src/store.js
  75. 41 0
      src/utills/gqlUtils.js
  76. 2 0
      src/utills/index.js
  77. 59 0
      src/utills/utils.js

+ 23 - 0
.gitignore

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

+ 5 - 0
.vscode/extensions.json

@@ -0,0 +1,5 @@
+{
+    "recommendations": [
+        "formulahendry.azure-storage-explorer"
+    ]
+}

+ 23 - 0
.vscode/launch.json

@@ -0,0 +1,23 @@
+{
+    // Use IntelliSense to learn about possible attributes.
+    // Hover to view descriptions of existing attributes.
+    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
+    "version": "0.2.0",
+    "configurations": [
+        {
+            "name": "Launch Chrome",
+            "request": "launch",
+            "type": "chrome",
+            "url": "http://localhost:3000",
+            "webRoot": "${workspaceFolder}"
+        },
+
+        {
+            "type": "chrome",
+            "request": "launch",
+            "name": "Launch Chrom against localhost",
+            "url": "http://localhost:3000",
+            "webRoot": "${workspaceFolder}"
+        }
+    ]
+}

+ 70 - 0
README.md

@@ -0,0 +1,70 @@
+# Getting Started with Create React App
+
+This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
+
+## Available Scripts
+
+In the project directory, you can run:
+
+### `npm start`
+
+Runs the app in the development mode.\
+Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
+
+The page will reload when you make changes.\
+You may also see any lint errors in the console.
+
+### `npm test`
+
+Launches the test runner in the interactive watch mode.\
+See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
+
+### `npm run build`
+
+Builds the app for production to the `build` folder.\
+It correctly bundles React in production mode and optimizes the build for the best performance.
+
+The build is minified and the filenames include the hashes.\
+Your app is ready to be deployed!
+
+See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
+
+### `npm run eject`
+
+**Note: this is a one-way operation. Once you `eject`, you can't go back!**
+
+If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
+
+Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
+
+You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
+
+## Learn More
+
+You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
+
+To learn React, check out the [React documentation](https://reactjs.org/).
+
+### Code Splitting
+
+This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
+
+### Analyzing the Bundle Size
+
+This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
+
+### Making a Progressive Web App
+
+This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
+
+### Advanced Configuration
+
+This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
+
+### Deployment
+
+This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
+
+### `npm run build` fails to minify
+
+This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)

File diff suppressed because it is too large
+ 20833 - 0
package-lock.json


+ 69 - 0
package.json

@@ -0,0 +1,69 @@
+{
+  "name": "shop-project",
+  "version": "0.1.0",
+  "private": true,
+  "dependencies": {
+    "@dnd-kit/core": "^6.0.7",
+    "@dnd-kit/modifiers": "^6.0.1",
+    "@dnd-kit/sortable": "^7.0.2",
+    "@emotion/react": "^11.10.5",
+    "@emotion/styled": "^11.10.5",
+    "@fontsource/roboto": "^4.5.8",
+    "@minoru/react-dnd-treeview": "^3.4.1",
+    "@mui/icons-material": "^5.11.0",
+    "@mui/lab": "^5.0.0-alpha.118",
+    "@mui/material": "^5.11.7",
+    "@mui/styled-engine-sc": "^5.11.0",
+    "@reduxjs/toolkit": "^1.9.1",
+    "@rtk-query/graphql-request-base-query": "^2.2.0",
+    "@testing-library/jest-dom": "^5.16.5",
+    "@testing-library/react": "^13.4.0",
+    "@testing-library/user-event": "^13.5.0",
+    "array-move": "^4.0.0",
+    "form-data": "^4.0.0",
+    "graphql-request": "^5.1.0",
+    "http-proxy-middleware": "^2.0.6",
+    "install": "^0.13.0",
+    "npm": "^9.3.1",
+    "react": "^18.2.0",
+    "react-dnd": "^16.0.1",
+    "react-dom": "^18.2.0",
+    "react-dropzone": "^14.2.3",
+    "react-lorem-ipsum": "^1.4.10",
+    "react-redux": "^8.0.5",
+    "react-router-dom": "^5.3.0",
+    "react-scripts": "5.0.1",
+    "redux": "^4.2.0",
+    "redux-persist": "^6.0.0",
+    "redux-thunk": "^2.4.2",
+    "redux-toolkit": "^1.1.2",
+    "rtk-query": "^0.0.0",
+    "styled-components": "^5.3.6",
+    "web-vitals": "^2.1.4"
+  },
+  "scripts": {
+    "start": "react-scripts start",
+    "build": "react-scripts build",
+    "test": "react-scripts test",
+    "eject": "react-scripts eject"
+  },
+  "eslintConfig": {
+    "extends": [
+      "react-app",
+      "react-app/jest"
+    ]
+  },
+  "browserslist": {
+    "production": [
+      ">0.2%",
+      "not dead",
+      "not op_mini all"
+    ],
+    "development": [
+      "last 1 chrome version",
+      "last 1 firefox version",
+      "last 1 safari version"
+    ]
+  }
+}
+

BIN
public/favicon.ico


+ 43 - 0
public/index.html

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

BIN
public/logo192.png


BIN
public/logo512.png


+ 25 - 0
public/manifest.json

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

+ 3 - 0
public/robots.txt

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

BIN
public/thememain.png


+ 38 - 0
src/App.css

@@ -0,0 +1,38 @@
+.App {
+  text-align: center;
+}
+
+.App-logo {
+  height: 40vmin;
+  pointer-events: none;
+}
+
+@media (prefers-reduced-motion: no-preference) {
+  .App-logo {
+    animation: App-logo-spin infinite 20s linear;
+  }
+}
+
+.App-header {
+  background-color: #282c34;
+  min-height: 100vh;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  font-size: calc(10px + 2vmin);
+  color: white;
+}
+
+.App-link {
+  color: #61dafb;
+}
+
+@keyframes App-logo-spin {
+  from {
+    transform: rotate(0deg);
+  }
+  to {
+    transform: rotate(360deg);
+  }
+}

+ 70 - 0
src/App.js

@@ -0,0 +1,70 @@
+import { Router, Route, Switch } from 'react-router-dom';
+import { store, persistedStore } from './store';
+import { Provider } from 'react-redux';
+import { CCategoriesList, CCategoryTree, CEditableCategory, CEditableGood, CGood, CGoodsList, CLoginForm, CMainAppBar, COrder, COrdersList, CRegisterForm, CUser, CUsersList } from "./Components";
+import { CLogout } from './Components';
+import { CSidebar } from './Components/Sidebar';
+import { CRootCats } from './Components';
+import './App.css';
+import { CCategory } from './Components/Category';
+import { CCart } from './Components/Cart';
+import { createBrowserHistory } from "history";
+import { PersistGate } from 'redux-persist/integration/react';
+import { Box, Typography } from '@mui/material';
+import backImage from "./images/theme_main.png"
+
+export const history = createBrowserHistory();
+
+
+const NotFound = () =>
+  <div>
+    <h1>404 not found</h1>
+  </div>
+
+
+const Main = () =>
+  <Box height="100vh" sx={{ backgroundImage: `url(${backImage})`, backgroundSize: "cover", backgroundRepeat: "no-repeat", backgroundPosition: "center center", marginTop: "-2.5vh" }}>
+    <h1>Main page</h1>
+  </Box>
+
+function App() {
+  return (
+    <>
+      <Router history={history}>
+        <Provider store={store}>
+          <PersistGate loading={<Typography>Loading...</Typography>} persistor={persistedStore}>
+            <div className="App">
+              <CMainAppBar />
+              <CSidebar id="sidBar" menuComponent={() => <CRootCats />} />
+              <Switch>
+                <Route path="/" component={Main} exact />
+                <Route path="/orders" component={COrdersList} />
+                <Route path="/users" component={CUsersList} />
+                <Route path="/goods" component={CGoodsList} />
+                <Route path="/good/:_id" component={CGood} />
+                <Route path="/editgood/:_id" component={CEditableGood} />
+                <Route path="/editgood" component={CEditableGood} />
+                <Route path="/category/:_id" component={CCategory} />
+                <Route path="/editcategory/:_id" component={CEditableCategory} />
+                <Route path="/editcategory" component={CEditableCategory} />
+                <Route path="/order/:_id" component={COrder} />
+                <Route path="/cart" component={CCart} />
+                <Route path="/login" component={CLoginForm} />
+                <Route path="/register" component={CRegisterForm} />
+                <Route path="/user/:_id" component={CUser} />
+                <Route path="/user" component={CUser} />
+                <Route path="/logout" component={CLogout} />
+                <Route path="/catree" component={CCategoryTree} />
+                <Route path="/categories" component={CCategoriesList} />
+                <Route path="*" component={NotFound} />
+              </Switch>
+            </div>
+          </PersistGate>
+        </Provider>
+      </Router>
+    </>
+
+  );
+}
+
+export default App;

+ 27 - 0
src/Components/AvatarAnimated.js

@@ -0,0 +1,27 @@
+import React from 'react';
+import { styled, alpha } from '@mui/material/styles';
+import { Avatar } from '@mui/material';
+
+export const AvatarImage = styled((props) => {
+    const { selected, ...other } = props;
+    return <Avatar {...other} />;
+})(({ theme, selected }) => ({
+    backgroundColor: selected && alpha(theme.palette.primary.main, 0.5),
+    boxShadow: selected && `0 0 0 1px ${alpha(theme.palette.primary.main, 0.5)}`,
+    avatarImage: {
+        objectFit: 'contain',
+    },
+}));
+
+export const AvatarAnimated = styled((props) => {
+    const { selected, ...other } = props;
+    return <AvatarImage {...other} />;
+})(({ theme, selected }) => ({
+    transition: theme.transitions.create('transform', {
+        duration: theme.transitions.duration.shortest,
+    }),
+    transform: selected && 'scale(1.1)',
+    '&:hover': {
+        transform: 'scale(1.1)',
+    },
+}));

+ 58 - 0
src/Components/Cart.js

@@ -0,0 +1,58 @@
+import React from 'react';
+import { Button, Typography } from "@mui/material"
+import { Box, Container } from "@mui/system"
+import { connect, useSelector } from "react-redux"
+import { getCurrentUser, useAddOrderMutation, useGetGoodsByIdQuery } from "../reducers"
+import { CartGoodsList } from "./CartGoodsList"
+import { findObjectIndexById } from '../utills';
+import { MyLink } from './MyLink';
+
+const mapCountToGood = (goodData, goodsCounts) => {
+    let count = 0;
+    let goodIdx = findObjectIndexById(goodsCounts, goodData._id);
+    if (goodIdx >= 0)
+        count = goodsCounts[goodIdx].count;
+    return count;
+}
+
+const Cart = () => {
+    let goods = useSelector(state => state.cart?.goods) ?? [];
+    let { isLoading, data } = useGetGoodsByIdQuery({ goods });
+    let goodsData = data?.GoodFind?.map(gd => ({ ...gd, count: mapCountToGood(gd, goods) })) ?? [];
+    let order = [];
+    for (let good of Object.values(goods)) {
+        order.push({ good: { _id: good._id }, count: good.count });
+    }
+    let currentUser = useSelector(state => getCurrentUser(state));
+    const [addOrderMutation, { isLoading: isOrderAdding }] = useAddOrderMutation();
+    return !isLoading && (
+        <>
+            <Container>
+                <Box>
+                    <Typography paragraph gutterBottom component={'h3'} variant={'h3'}>
+                        Cart
+                    </Typography>
+                    <CartGoodsList goods={goodsData ?? []} />
+                    {
+                        !currentUser ?
+                            <>
+                                <Typography>User not logged in. </Typography>
+                                <MyLink to='/login'>Please login</MyLink>
+
+                            </> :
+                            <Button size='small' color='primary' disabled={isOrderAdding || goodsData.length === 0}
+                                onClick={() => addOrderMutation({ order })}
+                            >
+                                Place Order
+                            </Button>
+                    }
+                </Box>
+            </Container>
+        </>
+    )
+}
+const CCart = connect(state => ({
+}),
+    {})(Cart);
+
+export { CCart };

+ 78 - 0
src/Components/CartGood.js

@@ -0,0 +1,78 @@
+import { Typography } from "@mui/material";
+import { getFullImageUrl } from "../utills";
+import { AvatarImage } from "./AvatarAnimated";
+import { StyledTableCell, StyledTableRow } from "./StyledTableElements";
+import "./cartGood.css"
+import { MyLink } from "./MyLink";
+import { connect } from "react-redux";
+import { actionAddGoodToCart, actionDeleteGoodFromCart } from "../reducers";
+import Button from '@mui/material/Button';
+import { DeleteOutline } from "@mui/icons-material";
+
+const CartGood = ({ good, goodNum, addToCart, deleteFromCart }) => {
+    return (
+        <>
+            <StyledTableRow>
+                <StyledTableCell item align="right" xs={1}>
+                    <Typography>
+                        {goodNum + 1}.
+                    </Typography>
+                </StyledTableCell>
+                <StyledTableCell item xs={2}>
+                    {good.images?.length > 0 ?
+                        <AvatarImage sx={{ width: 70, height: 70 }} variant='rounded' src={getFullImageUrl(good.images[0])} /> :
+                        null}
+                </StyledTableCell>
+                <StyledTableCell item xs={3}>
+                    {good?._id ?
+                        <MyLink to={`/good/${good?._id}`}>
+                            <Typography >
+                                {good.name}
+                            </Typography>
+                        </MyLink>
+                        :
+                        <Typography >
+                            {good.name}
+                        </Typography>
+                    }
+                </StyledTableCell>
+                <StyledTableCell item align="right" xs={2}>
+                    <Typography>
+                        {good.price}
+                    </Typography>
+                </StyledTableCell>
+                <StyledTableCell item align="right" xs={1}>
+                    <Button size='small' color='primary'
+                        onClick={() => addToCart(good, -1)}
+                    >
+                        -
+                    </Button>
+                    <Typography>
+                        {good.count}
+                    </Typography>
+                    <Button size='small' color='primary'
+                        onClick={() => addToCart(good, +1)}
+                    >
+                        +
+                    </Button>
+                </StyledTableCell>
+                <StyledTableCell item align="right" xs={1}>
+                    <Typography>
+                        {good.price * good.count}
+                    </Typography>
+                </StyledTableCell>
+                <StyledTableCell item align="right" xs={2}>
+                    <Button
+                        size="small"
+                        onClick={() => deleteFromCart(good)}
+                    >
+                        <DeleteOutline />
+                    </Button>
+                </StyledTableCell>
+            </StyledTableRow>
+        </>
+    )
+}
+const CCartGood = connect(state => ({}),
+    { addToCart: actionAddGoodToCart, deleteFromCart: actionDeleteGoodFromCart })(CartGood);
+export { CCartGood };

+ 61 - 0
src/Components/CartGoodsList.js

@@ -0,0 +1,61 @@
+import React from 'react';
+import { Paper } from '@mui/material';
+import { CCartGood } from './CartGood';
+import { Table, TableBody, TableContainer, TableHead, TableRow, TableCell } from "@mui/material";
+import { StyledTableCell } from './StyledTableElements';
+
+const CartGoodsList = ({ goods = [], tax_rate = 0 }) => {
+    function ccyFormat(num) {
+        return `${num.toFixed(2)}`;
+    }
+    function subtotal(items) {
+        return items.map(({ price, count }) => price * count).reduce((sum, i) => sum + i, 0);
+    }
+    const invoiceSubtotal = subtotal(goods);
+    const invoiceTaxes = tax_rate * invoiceSubtotal;
+    const invoiceTotal = invoiceTaxes + invoiceSubtotal;
+
+    return (
+        <>
+            <TableContainer component={Paper} sx={{ minWidth: 700, maxWidth: 1200 }} >
+                <Table aria-label="customized table">
+                    <TableHead>
+                        <TableRow>
+                            <StyledTableCell align="right">#</StyledTableCell>
+                            <StyledTableCell></StyledTableCell>
+                            <StyledTableCell>Name</StyledTableCell>
+                            <StyledTableCell align="right">Price ($)</StyledTableCell>
+                            <StyledTableCell align="right">Count</StyledTableCell>
+                            <StyledTableCell align="right">Total</StyledTableCell>
+                            <StyledTableCell align="right"></StyledTableCell>
+                        </TableRow>
+                    </TableHead>
+                    <TableBody>
+                        {
+                            goods.map((good, index) => {
+                                return (
+                                    <CCartGood key={good._id} good={good} goodNum={index} maxWidth='xs' />
+                                )
+                            })
+                        }
+                        <TableRow>
+                            <TableCell rowSpan={3} colSpan={3} />
+                            <TableCell colSpan={2}>Subtotal</TableCell>
+                            <TableCell align="right">{ccyFormat(invoiceSubtotal)}</TableCell>
+                        </TableRow>
+                        <TableRow>
+                            <TableCell>Tax</TableCell>
+                            <TableCell align="right">{`${(tax_rate * 100).toFixed(0)} %`}</TableCell>
+                            <TableCell align="right">{ccyFormat(invoiceTaxes)}</TableCell>
+                        </TableRow>
+                        <TableRow>
+                            <TableCell colSpan={2}>Total</TableCell>
+                            <TableCell align="right">{ccyFormat(invoiceTotal)}</TableCell>
+                        </TableRow>
+                    </TableBody>
+                </Table>
+            </TableContainer>
+        </>
+    )
+}
+export { CartGoodsList };

+ 94 - 0
src/Components/Category.js

@@ -0,0 +1,94 @@
+import { List, ListItem, ListItemButton, ListItemText, Button } from "@mui/material"
+import { Typography, Grid } from "@mui/material"
+import { Box, Container } from "@mui/system"
+import { useEffect } from "react"
+import { connect, useDispatch, useSelector } from "react-redux"
+import { useParams } from "react-router-dom"
+import { MyLink } from "."
+import { isCurrentUserAdmin, useGetCategoryByIdQuery } from "../reducers"
+import { actionSetCurrentEntity, frontEndNames, getCurrentEntity } from "../reducers/frontEndReducer"
+import { CategoryBreadcrumbs } from "./CategoryBreadcrumbs"
+import { CGoodsList } from "./GoodsList"
+import { LoadingState } from "./LoadingState"
+import { CatsList } from "./RootCats"
+
+const CSubCategories = connect(state => ({ cats: getCurrentEntity(frontEndNames.category, state)?.subCategories }),
+    {})(CatsList);
+
+const Category = () => {
+    const { _id } = useParams();
+    const { isLoading, data } = useGetCategoryByIdQuery(_id);
+    let cat = isLoading ? { name: 'loading', goods: [] } : data?.CategoryFindOne;
+    let csubCats = false;
+    const dispatch = useDispatch();
+    let state = useSelector(state => state);
+    useEffect(() => {
+        if (getCurrentEntity(frontEndNames.category, state)?._id !== _id)
+            dispatch(actionSetCurrentEntity(frontEndNames.category, { _id }));
+        if (!isLoading)
+            dispatch(actionSetCurrentEntity(frontEndNames.category, data.CategoryFindOne));
+    }, [_id, isLoading, data]);
+    let isAdmin = isCurrentUserAdmin(state);
+    return isLoading ? <LoadingState /> : (
+        <>
+            <Container>
+                <Box>
+                    <CategoryBreadcrumbs category={cat} />
+                    {
+                        isAdmin && (
+                            <>
+                                <Grid container spacing={2} justifyContent="center">
+                                    <Grid item>
+                                        <MyLink to="/editgood">
+                                            <Button size='small' variant="contained" >
+                                                Add Good
+                                            </Button>
+                                        </MyLink>
+                                    </Grid>
+                                    <Grid item>
+                                        <MyLink to={`/editcategory/${cat._id}`}>
+                                            <Button size='small' variant="contained" >
+                                                Edit Category
+                                            </Button>
+                                        </MyLink>
+                                    </Grid>
+                                </Grid>
+                            </>
+                        )
+                    }
+                    <Typography paragraph gutterBottom component={'h3'} variant={'h3'} sx={{ marginTop: "1vh" }} >
+                        {cat.name}
+                    </Typography>
+                    {csubCats && <CSubCategories />}
+                    {!csubCats && cat.subCategories?.length > 0 && (
+                        <List>
+                            {cat.subCategories.map(scat => (
+                                <ListItem key={scat._id} disablePadding>
+                                    <ListItemButton>
+                                        <MyLink to={`/category/${scat._id}`} >
+                                            <ListItemText
+                                                disableTypography
+                                                primary={
+                                                    <Typography paragraph gutterBottom component={'h4'} variant={'h4'} sx={{ marginTop: "1vh", marginLeft: "18vh" }} >
+                                                        {scat.name}
+                                                    </Typography>
+                                                }
+                                            />
+                                        </MyLink>
+                                    </ListItemButton>
+                                </ListItem>
+                            ))}
+                        </List>
+                    )
+                    }
+                    <CGoodsList goods={cat.goods} />
+                </Box>
+            </Container>
+        </>
+    )
+}
+
+const CCategory = connect(state => ({}),
+    {})(Category);
+
+export { CCategory };

+ 35 - 0
src/Components/CategoryBreadcrumbs.js

@@ -0,0 +1,35 @@
+import { Breadcrumbs } from "@mui/material";
+import { Typography } from "@mui/material";
+import { MyLink } from ".";
+
+export const CategoryBreadcrumbs = ({ category, showLeafAsLink = false }) => {
+    return (
+        <Breadcrumbs aria-label="breadcrumb">
+            <MyLink underline="hover" color="inherit" to="/">
+                Home
+            </MyLink>
+            {
+                category.parent?._id && (
+                    <MyLink
+                        underline="hover"
+                        color="inherit"
+                        to={`/category/${category.parent?._id}`}
+                    >
+                        {category.parent?.name}
+                    </MyLink>
+                )}
+            {
+                showLeafAsLink ?
+                        <MyLink
+                            underline="hover"
+                            color="inherit"
+                            to={`/category/${category._id}`}
+                        >
+                            {category.name}
+                        </MyLink>
+                    :
+                    <Typography color="text.primary">{category.name}</Typography>
+            }
+        </Breadcrumbs>
+    );
+};

+ 135 - 0
src/Components/CategoryList.js

@@ -0,0 +1,135 @@
+import React from 'react';
+import { Container, Typography, Paper, Avatar, Button } from '@mui/material';
+import { Table, TableBody, TableContainer, TableHead, TableRow } from "@mui/material";
+import { StyledTableCell, StyledTableRow } from './StyledTableElements';
+import { CPagination } from './Pagination';
+import { CSearchInput } from './SearchInput';
+import { MyLink, ReferenceLink } from '.';
+import { useSelector } from 'react-redux';
+import { frontEndNames, getCurrentUser, getEntitiesListShowParams, useGetCategoriesCountQuery, useGetCategoriesQuery } from '../reducers';
+import { UserEntity } from '../Entities';
+import { getFullImageUrl } from '../utills';
+
+const CategoriesList = ({ entities, entitiesTypeName, fromPage, pageSize, isAdmin }) => {
+
+    let headCells = [
+        {
+            id: '#',
+            numeric: true,
+            disablePadding: true,
+            label: '#',
+            align: "center"
+        },
+        {
+            id: 'Image',
+            numeric: false,
+            disablePadding: true,
+            label: 'Image',
+            align: "center"
+        },
+        {
+            id: 'Name',
+            numeric: false,
+            disablePadding: true,
+            label: 'Name',
+        },
+
+        {
+            id: 'Owner',
+            numeric: false,
+            disablePadding: true,
+            label: 'Owner',
+            align: "right"
+        },
+        {
+            id: 'Parent',
+            numeric: false,
+            disablePadding: true,
+            label: 'Parent',
+            align: "right"
+        },
+    ]
+    return (
+        <>
+            <Container maxWidth="lg" sx={{marginTop: "1vh"}}>
+                {
+                    isAdmin && (
+                        <MyLink to="/editcategory">
+                            <Button size='small' variant="contained" >
+                                Add Category
+                            </Button>
+                        </MyLink>
+                    )
+                }
+                <CSearchInput entitiesTypeName={entitiesTypeName} />
+                <TableContainer component={Paper} >
+                    <Table sx={{ overflow: 'scroll' }} >
+                        <TableHead>
+                            <TableRow>
+                                {
+                                    headCells.map(headCell => {
+                                        return <StyledTableCell key={headCell.id} align={headCell.align}>{headCell.label}</StyledTableCell>
+                                    })
+                                }
+                            </TableRow>
+                        </TableHead>
+                        {entities?.length > 0 && (
+                            <TableBody>
+                                {
+                                    entities.map((entity, index) => {
+                                        return (
+                                            <StyledTableRow key={entity._id}>
+                                                <StyledTableCell align="right" >
+                                                    <Typography>
+                                                        {(fromPage * pageSize) + index + 1}.
+                                                    </Typography>
+                                                </StyledTableCell>
+                                                <StyledTableCell align="right" >
+                                                    <Avatar variant='rounded' key={index} src={getFullImageUrl(entity.image)} />
+                                                </StyledTableCell>
+                                                <StyledTableCell  >
+                                                    <MyLink to={`/category/${entity._id}`}>
+                                                        <Typography >
+                                                            <ReferenceLink entity={entity} path='editcategory' getText={ref => ref?.name || "<no name>"} />
+                                                        </Typography>
+                                                    </MyLink>
+                                                </StyledTableCell>
+                                                <StyledTableCell align="right" >
+                                                    {
+                                                        <ReferenceLink entity={entity} refName='owner' typeName={frontEndNames.users} getText={ref => ref ? ref.nick || ref.login : "No owner"} />
+                                                    }
+                                                </StyledTableCell>
+                                                <StyledTableCell align="right" >
+                                                    {
+                                                        <ReferenceLink entity={entity} refName='parent' path='editablecategory' getText={ref => ref ? ref.name : "ROOT"} />
+                                                    }
+                                                </StyledTableCell>
+                                            </StyledTableRow>
+                                        )
+                                    })
+                                }
+                            </TableBody>
+                        )}
+                    </Table>
+                    <CPagination entitiesTypeName={entitiesTypeName} />
+                </TableContainer>
+            </Container>
+        </>
+    )
+
+}
+const CCategoriesList = () => {
+    let entitiesTypeName = frontEndNames.category;
+    let state = useSelector(state => state);
+    const { fromPage, pageSize, searchStr } = getEntitiesListShowParams(entitiesTypeName, state);
+    let currentUser = useSelector(state => new UserEntity(getCurrentUser(state)));
+
+    const categoriesResult = useGetCategoriesQuery({ withParent: true, withChildren: true, withOwner: true, fromPage, pageSize, searchStr, owner: currentUser });
+    const categoriesCountResult = useGetCategoriesCountQuery({ searchStr, owner: currentUser }, { refetchOnMountOrArgChange: true });
+    let isLoading = categoriesResult.isLoading || categoriesCountResult.isLoading;
+
+    let entities = !isLoading && categoriesResult.data?.CategoryFind;
+    return !isLoading && <CategoriesList entities={entities} isAdmin={currentUser.isAdminRole} entitiesTypeName={entitiesTypeName} fromPage={fromPage} pageSize={pageSize} />
+}
+
+export { CCategoriesList };

+ 151 - 0
src/Components/CategoryTree.js

@@ -0,0 +1,151 @@
+import React, { useState } from "react";
+import { DndProvider } from "react-dnd";
+import {
+    Tree,
+    MultiBackend,
+    getBackendOptions
+} from "@minoru/react-dnd-treeview";
+import { DefaultSubCategoriesTreeDepth, useGetRootCategoriesQuery, useSaveCategoryMutation } from "../reducers";
+import { CategoryTreeItem } from "./CategoryTreeItem";
+import { ThemeProvider, CssBaseline } from "@mui/material";
+import { createTheme } from "@mui/material/styles";
+import styles from "./CategoryTree.module.css";
+
+export const theme = createTheme({
+    components: {
+        MuiCssBaseline: {
+            styleOverrides: {
+                "*": {
+                    margin: 0,
+                    padding: 0
+                },
+                "html, body, #root": {
+                    height: "100%"
+                },
+                ul: {
+                    listStyle: "none"
+                }
+            }
+        },
+        MuiSvgIcon: {
+            styleOverrides: {
+                root: { verticalAlign: "middle" }
+            }
+        }
+    }
+});
+
+
+const CategoryTree = ({ elements, saveCategory }) => {
+    console.log(elements);
+    const [treeData, setTreeData] = useState(elements);
+
+    const handleDrop = (newTree, params) => {
+        let targetCat = params.dropTarget?.cat;
+        let sourceCat = params.dragSource?.cat;
+        if (!sourceCat)
+            throw new Error("No source");
+
+        if (sourceCat.parent?._id !== targetCat?._id) {
+            let parentCat = params.dragSource.parentCat;
+            if (parentCat)
+                parentCat.subCategories = parentCat.subCategories.filter(sc => sc?.cat._id !== sourceCat._id);
+            if (!params.dropTarget.subCategories)
+                params.dropTarget.subCategories = [];
+            params.dropTarget.subCategories.push(params.dragSource);
+            params.dragSource.parentCat = params.dropTarget;
+
+            sourceCat.parent = targetCat ?? null;
+            saveCategory(sourceCat);
+            setTreeData(newTree);
+        }
+    }
+    return (
+        <ThemeProvider theme={theme}>
+            <CssBaseline />
+            <DndProvider backend={MultiBackend} options={getBackendOptions()}>
+                <div className={styles.app}>
+                    <Tree
+                        style={{ listStyleType: 'none', paddingLeft: '0px', height: 240, flexGrow: 1, maxWidth: 400, overflowY: 'auto' }}
+                        tree={treeData}
+                        rootId={0}
+                        initialOpen={[1]}
+                        render={(node, { depth, isOpen, onToggle }) => (
+                            <CategoryTreeItem
+                                node={node}
+                                depth={depth}
+                                isOpen={isOpen}
+                                onToggle={onToggle}
+                                saveCategoryName={(node, name) => {
+                                    if (!node?.cat || node.cat.name === name)
+                                        return;
+                                    node.text = node.cat.name = name;
+                                    saveCategory(node.cat);
+                                }}
+                            />
+                        )}
+                        dragPreviewRender={(monitorProps) => (
+                            <CategoryTreeItem node={monitorProps.item} depth={0} isOpen={false}/>
+                        )}
+                        onDrop={handleDrop}
+                        classes={{
+                            root: styles.treeRoot,
+                            draggingSource: styles.draggingSource,
+                            dropTarget: styles.dropTarget
+                        }}
+                    />
+                </div>
+            </DndProvider>
+        </ThemeProvider>
+    );
+}
+
+let index = 2;
+function wrapToTreeItems(cats, parentCat = null, catTreeItems = undefined) {
+    catTreeItems ??= [];
+    if (cats) {
+        for (let cat of cats) {
+            let catTreeItem = {
+                "id": index++,
+                "parent": parentCat?.id ?? 1,
+                "parentCat": parentCat,
+                "droppable": true,
+                "text": cat.name,
+                "image": cat.image ?? {},
+                "cat": { _id: cat._id, name: cat.name, parent: parentCat?.cat ?? null },
+            };
+            if (!parentCat.subCategories)
+                parentCat.subCategories = [];
+            parentCat.subCategories.push(catTreeItem);
+            catTreeItems.push(catTreeItem);
+            wrapToTreeItems(cat.subCategories, catTreeItem, catTreeItems)
+        }
+    }
+    return catTreeItems;
+}
+
+const CCategoryTree = () => {
+    const { isLoading, data } = useGetRootCategoriesQuery(DefaultSubCategoriesTreeDepth);
+    let cats = data?.CategoryFind;
+
+    let catTreeItems = [];
+    let rootCat = {
+        id: 1,
+        parent: 0,
+        droppable: true,
+        text: "...",
+        cat: null
+    }
+    catTreeItems.push(rootCat);
+
+    const [saveCategoryMutation] = useSaveCategoryMutation(true);
+
+    const saveCategory = async (category) => {
+        await saveCategoryMutation({ category });
+    }
+
+    return !isLoading && cats && <CategoryTree elements={wrapToTreeItems(cats, rootCat, catTreeItems)} saveCategory={saveCategory} />
+}
+
+export { CCategoryTree };
+

+ 15 - 0
src/Components/CategoryTree.module.css

@@ -0,0 +1,15 @@
+.app {
+    height: 100%;
+}
+
+.treeRoot {
+    height: 100%;
+}
+
+.draggingSource {
+    opacity: .3;
+}
+
+.dropTarget {
+    background-color: #e8f0fe;
+}

+ 71 - 0
src/Components/CategoryTreeItem.js

@@ -0,0 +1,71 @@
+import React from "react";
+import { TextField } from "@mui/material";
+import ArrowRightIcon from "@mui/icons-material/ArrowRight";
+import styles from "./CategoryTreeItem.module.css";
+import { useDragOver } from "@minoru/react-dnd-treeview";
+import { AvatarImage } from "./AvatarAnimated";
+import { getFullImageUrl } from "../utills";
+import CategoryIcon from '@mui/icons-material/Category';
+
+export const CategoryTreeItem = (props) => {
+    const { id, droppable, data } = props.node;
+    const indent = props.depth * 40;
+
+    const handleToggle = (e) => {
+        e.stopPropagation();
+        props.onToggle(props.node.id);
+    };
+
+    const dragOverProps = useDragOver(id, props.isOpen, props.onToggle);
+
+    return (
+        <div
+            className={`tree-node ${styles.root}`}
+            style={{ paddingInlineStart: indent }}
+            {...dragOverProps}
+        >
+            <div
+                className={`${styles.expandIconWrapper} ${props.isOpen ? styles.isOpen : ""
+                    }`}
+            >
+                {props.node.droppable && props.node.subCategories?.length > 0 && (
+                    <div onClick={handleToggle}>
+                        <ArrowRightIcon color="secondary" />
+                    </div>
+                )}
+            </div>
+            <div>
+                {
+                    props.node.image?.url ?
+                        <AvatarImage variant='rounded' src={getFullImageUrl(props.node.image)} />
+                        :
+                        <AvatarImage variant='rounded' sx={{ bgcolor: "rgba(184, 200, 239, 0.63)" }}><CategoryIcon color="secondary"/></AvatarImage>
+                }
+
+            </div>
+            <div className={styles.labelGridItem}>
+                <TextField
+                    color="primary"
+                    InputProps={{
+                        sx: {
+                            "& input": {
+                                fontSize: "1.5em",
+                                display: 'flex',
+                                alignItems: 'center',
+                                color: 'darkBlue',
+                                fontWeight: 'bold',
+                            }
+                        },
+                    }}
+                    id="filled-basic" defaultValue={props.node.text} size="small"
+                    onBlur={e => props.saveCategoryName(props.node, e.target.value)}
+                    onKeyUp={(e) => {
+                        if (e.key === 'Escape') {
+                            e.target.value = e.target.defaultValue;
+                        }
+                    }}
+                />
+            </div>
+        </div>
+    );
+};

+ 30 - 0
src/Components/CategoryTreeItem.module.css

@@ -0,0 +1,30 @@
+.root {
+    align-items: center;
+    display: grid;
+    grid-template-columns: auto auto 1fr auto;
+    height: 62px;
+    padding-inline-end: 8px;
+  }
+  
+  .expandIconWrapper {
+    align-items: center;
+    font-size: 0;
+    cursor: pointer;
+    display: flex;
+    height: 50px;
+    justify-content: center;
+    width: 50px;
+    transition: transform linear .1s;
+    transform: rotate(0deg);
+  }
+  
+  .expandIconWrapper.isOpen {
+    transform: rotate(90deg);
+  }
+  
+  .labelGridItem {
+    padding-inline-start: 25px;
+    text-align:left;
+  }
+
+

+ 143 - 0
src/Components/DropDownList.js

@@ -0,0 +1,143 @@
+import React, { useEffect } from 'react';
+import { Button, ButtonGroup, ClickAwayListener, Grow, Paper, Popper, MenuItem, MenuList, Typography } from "@mui/material";
+import { DefaultSubCategoriesTreeDepth, useGetRootCategoriesQuery } from '../reducers';
+import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
+import { styled } from '@mui/material/styles';
+import { DeleteOutline } from '@mui/icons-material';
+
+const DropDownList = ({ elements, selectedIndex: selectedIndexExt, onSetCategory, showClearButton = false }) => {
+    const [open, setOpen] = React.useState(false);
+    const anchorRef = React.useRef(null);
+    const [selectedIndex, setSelectedIndex] = React.useState(selectedIndexExt);
+    useEffect(() =>{
+        setSelectedIndex(selectedIndexExt);
+    }, [selectedIndexExt])
+
+    const handleClick = () => {
+        handleToggle();
+    };
+
+    const handleMenuItemClick = (event, index) => {
+        setSelectedIndex(index);
+        onSetCategory(index < 0 ? undefined : elements[index]);
+        setOpen(false);
+    };
+
+    const handleToggle = () => {
+        setOpen((prevOpen) => !prevOpen);
+    };
+
+    const handleClose = (event) => {
+        if (anchorRef.current && anchorRef.current.contains(event.target)) {
+            return;
+        }
+        setOpen(false);
+    };
+
+    return (
+        <React.Fragment>
+            <ButtonGroup sx={{ width: "100%" }} variant="contained" ref={anchorRef} aria-label="split button">
+                <Button onClick={handleClick} sx={{ width: "100%" }}>{selectedIndex >= 0 ? elements[selectedIndex].render : <></>}</Button>
+                <Button
+                    size="small"
+                    aria-controls={open ? 'split-button-menu' : undefined}
+                    aria-expanded={open ? 'true' : undefined}
+                    aria-label="select merge strategy"
+                    aria-haspopup="menu"
+                    onClick={handleToggle}
+                >
+                    <ArrowDropDownIcon />
+                </Button>
+                {
+                    showClearButton ?
+                        <Button
+                            size="small"
+                            onClick={() => handleMenuItemClick(undefined, -1)}
+                        >
+                            <DeleteOutline />
+                        </Button>
+                        : <></>
+                }
+            </ButtonGroup>
+            <Popper
+                sx={{
+                    zIndex: 1,
+                }}
+                open={open}
+                anchorEl={anchorRef.current}
+                role={undefined}
+                transition
+                disablePortal
+            >
+                {({ TransitionProps, placement }) => (
+                    <Grow
+                        {...TransitionProps}
+                        style={{
+                            transformOrigin:
+                                placement === 'bottom' ? 'center top' : 'center bottom',
+                        }}
+                    >
+                        <Paper>
+                            <ClickAwayListener onClickAway={handleClose}>
+                                <MenuList id="split-button-menu" autoFocusItem>
+                                    {elements.map((element, index) => (
+                                        <MenuItem
+                                            key={element.key}
+                                            selected={index === selectedIndex}
+                                            onClick={(event) => handleMenuItemClick(event, index)}
+                                        >
+                                            {element.render}
+                                        </MenuItem>
+                                    ))}
+                                </MenuList>
+                            </ClickAwayListener>
+                        </Paper>
+                    </Grow>
+                )}
+            </Popper>
+        </React.Fragment>
+    );
+}
+
+
+const CategoryItem = ({ catItem }) => {
+
+    return <Typography>{catItem.prefix + ' ' + catItem.name}</Typography>
+}
+
+const wrapToTreeItems = (cats, parentCat = undefined, currentLevelPrefix = '', catTreeItems = undefined) => {
+    catTreeItems ??= [];
+    if (cats) {
+        for (let i = 0; i < cats.length; i++) {
+            let cat = cats[i];
+            let catTreeItem = {
+                key: cat._id,
+                prefix: currentLevelPrefix + `${i + 1}.`,
+                name: cat.name,
+                cat: cat,
+                parent: parentCat?.id,
+            };
+            catTreeItem.render = <CategoryItem catItem={catTreeItem} />;
+            catTreeItems.push(catTreeItem);
+            wrapToTreeItems(cat.subCategories, catTreeItem, catTreeItem.prefix, catTreeItems)
+        }
+    }
+    return catTreeItems;
+}
+
+export const CCategoryDropDownListUnstyled = ({ currentCat, onSetCategory, showClearButton }) => {
+    const { isLoading, data } = useGetRootCategoriesQuery(DefaultSubCategoriesTreeDepth);
+    let cats = data?.CategoryFind;
+    if (!isLoading && cats) {
+        cats = wrapToTreeItems(cats);
+        let selectedIndex = cats.findIndex(c => c.key == currentCat?._id);
+        return <DropDownList elements={cats} selectedIndex={selectedIndex} onSetCategory={onSetCategory} showClearButton={showClearButton} />
+    }
+}
+
+
+export const CCategoryDropDownList = styled(CCategoryDropDownListUnstyled)`
+    .MenuItem {
+        background-color: solid palegreen;
+    }
+`

+ 120 - 0
src/Components/EditableCategory.js

@@ -0,0 +1,120 @@
+import { useSelector } from "react-redux"
+import { useParams } from "react-router-dom";
+import { Button, Card, CardActions, CardContent, CardMedia, Container, Grid, TextField, Typography } from "@mui/material";
+import { useState } from "react";
+import { getFullImageUrl, saveImage } from "../utills/utils";
+import { isCurrentUserAdmin, useGetCategoryByIdQuery, useSaveCategoryMutation } from "../reducers";
+import { CCategoryDropDownList } from "./DropDownList";
+
+const EditableCategory = ({ category: categoryExt, maxWidth = 'md', saveCategory }) => {
+    const copyCategory = category => ({ ...category });
+
+    let [category, setCategory] = useState(copyCategory(categoryExt));
+
+    const onSetParentCategory = (parentCat) => {
+        let cat = parentCat?.cat;
+        return setCategoryData({ parent: cat ? { _id: cat._id, name: cat.name } : undefined });
+    }
+
+    const setCategoryData = (data) => {
+        let categoryData = copyCategory({ ...category, ...data });
+        setCategory(categoryData);
+        return categoryData;
+    }
+    const saveFullCategory = async () => {
+        saveCategory({ category: { _id: category._id, name: category.name, parent: category.parent ?? null, image: { _id: category.image?._id } ?? null } });
+    }
+
+    const uploadAvatar = async param => {
+        let image = await saveImage({ data: param.target.files[0] }, false);
+        setCategoryData({ image });
+    }
+
+    return category && (
+        <>
+            <Container maxWidth={maxWidth}>
+                <Card variant='outlined'>
+                    <Grid container spacing={maxWidth === 'xs' ? 7 : 5} rowSpacing={2}>
+                        <Grid item xs={12}>
+                            <Grid container spacing={2}>
+                                <Grid item xs={4}>
+                                    <CardMedia
+                                        component="img"
+                                        sx={{ height: 300, padding: "1em 1em 0 1em", objectFit: "contain" }}
+                                        image={getFullImageUrl(category.image)}
+                                        title={category.name}
+                                    />
+                                    <Button
+                                        variant="contained"
+                                        component="label"
+                                    >
+                                        Upload File
+                                        <input
+                                            type="file"
+                                            hidden
+                                            onChange={param => uploadAvatar(param)}
+                                        />
+                                    </Button>
+                                </Grid>
+                                <Grid item xs={8}>
+                                    <CardContent>
+                                        <Grid container rowSpacing={2}>
+                                            <Grid item width="100%">
+                                                {
+                                                    <CCategoryDropDownList currentCat={category.parent} onSetCategory={onSetParentCategory} showClearButton={true} />
+                                                }
+                                            </Grid>
+                                            <Grid item width="100%">
+                                                <TextField
+                                                    required
+                                                    id="outlined-required"
+                                                    label="Name"
+                                                    value={category.name}
+                                                    onChange={event => setCategoryData({ name: event.target.value })}
+                                                    fullWidth
+                                                />
+                                            </Grid>
+                                        </Grid>
+                                    </CardContent>
+                                </Grid>
+                            </Grid>
+                        </Grid>
+                    </Grid>
+                    <CardActions>
+                        <Button size='small' color='primary'
+                            onClick={() => saveFullCategory(category)}
+                        >
+                            Save
+                        </Button>
+                        <Button size='small' color='primary'
+                            onClick={() => setCategory(copyCategory(categoryExt))}
+                        >
+                            Cancel
+                        </Button>
+                    </CardActions>
+                </Card>
+            </Container >
+        </>
+    )
+}
+
+const CEditableCategory = ({ maxWidth = 'md' }) => {
+    const { _id } = useParams();
+    const { isLoading, data } = useGetCategoryByIdQuery(_id);
+    const [saveCategoryMutation, { }] = useSaveCategoryMutation();
+
+    let category = isLoading ? undefined : data?.CategoryFindOne;
+    let state = useSelector(state => state)
+    let isAdmin = isCurrentUserAdmin(state);
+
+    if (!_id && isAdmin) {
+        category = { _id: undefined, name: undefined, parent: null };
+    }
+
+    return (!isLoading || !_id) && category && isAdmin ? (
+        <EditableCategory category={category} maxWidth={maxWidth} saveCategory={saveCategoryMutation} />) :
+        isLoading ? <Typography>Loading</Typography> : <Typography>Permission denied</Typography>;
+}
+
+
+export { CEditableCategory }

+ 180 - 0
src/Components/EditableGood.js

@@ -0,0 +1,180 @@
+import React, { useState } from 'react';
+import Button from '@mui/material/Button';
+import { Container, Grid, Card, CardContent, CardMedia, CardActions, TextField, InputAdornment, Box, Modal } from '@mui/material';
+import { getFullImageUrl } from "./../utills";
+import { useSelector } from 'react-redux';
+import { frontEndNames, getCurrentEntity, isCurrentUserAdmin, useGetGoodByIdQuery, useSaveGoodMutation } from '../reducers';
+import { useParams } from 'react-router-dom';
+import { CSortedFileDropZone } from './SortedFileDropZone';
+import { saveImage } from '../utills/utils';
+import { CGood } from './Good';
+import { ModalContainer } from './ModalContainer';
+import { history } from "../App";
+import { LackPermissions } from './LackPermissions';
+import { CCategoryDropDownList } from './DropDownList';
+import { CategoryBreadcrumbs } from './CategoryBreadcrumbs';
+
+
+const EditableGood = ({ good: goodExt, maxWidth = 'md', saveGood }) => {
+    const copyGood = goodExt => ({ ...goodExt, images: [...(goodExt.images ?? [])] });
+    let [good, setGood] = useState(copyGood(goodExt));
+    let [showPreview, setShowPreview] = useState(false);
+    let [imagesContainer, setImagesContainer] = useState({ images: [...(goodExt.images ?? [])] });
+
+    const onSetCategory = (catItem) => {
+        if (!catItem.cat.name)
+            throw new Error("Category must have name.");
+        good.categories = catItem.cat ? [{ _id: catItem.cat._id, name: catItem.cat.name }] : [];
+    }
+    const setGoodData = (data) => {
+        let goodData = { ...good, ...data };
+        setGood(goodData);
+        return goodData;
+    }
+    const onChangeImages = async images => {
+        let addedImages =  images.filter(img => !img._id);
+        let results = await Promise.all(addedImages.map(img => saveImage(img)));
+        for (let i = 0; i < results.length; i++) {
+            addedImages[i]._id = results[i]._id;
+            addedImages[i].url = results[i].url;
+        }
+
+        setImagesContainer({ images });
+        good.images = images;
+        setGood(good);
+    }
+    const preview = show => {
+        setShowPreview(show);
+    }
+
+    let isExistingGood = good?._id;
+    const saveFullGood = async () => {
+        let images = imagesContainer.images.map(img => ({ _id: img._id }));
+        good = { ...good, images };
+        saveGood({ good })
+            .then(res => {
+                let _id = res.data?.GoodUpsert?._id;
+                if (_id && !isExistingGood) {
+                    history.push(`/editgood/${_id}`);
+                }
+                return res;
+            });
+    }
+    if (good)
+        good.categories ??= [];
+    const currentCategory = good.categories?.length > 0 ? good.categories[0] : undefined;
+    return good && (
+        <>
+            <Container maxWidth={maxWidth}>
+                <CategoryBreadcrumbs category={currentCategory} showLeafAsLink={true} />
+                <Card variant='outlined'>
+                    <Grid container spacing={maxWidth === 'xs' ? 7 : 5} rowSpacing={2}>
+                        <Grid item xs={12}>
+                            <Grid container spacing={2}>
+                                <Grid item xs={4}>
+                                    <CardMedia
+                                        component="img"
+                                        sx={{ height: 300, padding: "1em 1em 0 1em", objectFit: "contain" }}
+                                        image={good.images?.length > 0 ? getFullImageUrl(good.images[0]) : ''}
+                                        title={good.name}
+                                    />
+                                </Grid>
+                                <Grid item xs={8}>
+                                    <CardContent>
+                                        <Grid container rowSpacing={2}>
+                                            <Grid item width="100%">
+                                                {
+                                                    <CCategoryDropDownList currentCat={currentCategory} onSetCategory={onSetCategory} />
+                                                }
+                                            </Grid>
+                                            <Grid item width="100%">
+                                                <TextField
+                                                    required
+                                                    id="outlined-required"
+                                                    label="Name"
+                                                    value={good.name}
+                                                    onChange={event => setGoodData({ name: event.target.value })}
+                                                    fullWidth
+                                                />
+                                            </Grid>
+                                            <Grid item width="100%">
+                                                <TextField
+                                                    required
+                                                    id="outlined-required"
+                                                    label="Price"
+                                                    type="number"
+                                                    startAdornment={<InputAdornment position="start">$</InputAdornment>}
+                                                    value={good.price}
+                                                    onChange={event => setGoodData({ price: +event.target.value })}
+                                                    fullWidth
+                                                />
+                                            </Grid>
+                                            <Grid item width="100%">
+                                                <TextField
+                                                    required
+                                                    id="outlined-required"
+                                                    label="Description"
+                                                    value={good.description}
+                                                    onChange={event => setGoodData({ description: event.target.value })}
+                                                    multiline={true}
+                                                    rows={15}
+                                                    fullWidth
+                                                />
+                                            </Grid>
+                                        </Grid>
+                                    </CardContent>
+                                </Grid>
+                            </Grid>
+                        </Grid>
+                    </Grid>
+                    {showPreview &&
+                        <ModalContainer onCloseClick={() => preview(false)}>
+                            <CGood good={good} editable={false} />
+                        </ModalContainer>
+                    }
+                    <CardActions>
+                        <Button size='small' color='primary'
+                            onClick={() => saveFullGood(good)}
+                        >
+                            Save
+                        </Button>
+                        <Button size='small' color='primary'
+                            onClick={() => setGood(copyGood(goodExt))}
+                        >
+                            Cancel
+                        </Button>
+                        <Button size='small' color='primary'
+                            onClick={() => preview(true)}
+                        >
+                            Preview
+                        </Button>
+                    </CardActions>
+                    <CSortedFileDropZone items={good.images} sx={{ maxWidth: "15vh" }} itemProp={{ sx: { maxWidth: "15vh" } }} onChange={items => onChangeImages(items)} />
+                </Card>
+            </Container>
+        </>
+    )
+}
+
+const CEditableGood = ({ maxWidth = 'md' }) => {
+    const { _id } = useParams();
+    let { isLoading, data } = useGetGoodByIdQuery(_id || 'fwkjelnfvkjwe');
+    isLoading = _id ? isLoading : false;
+    let good = isLoading ? { name: 'loading', categories: [] } : data?.GoodFindOne;
+    const [saveGoodMutation, { }] = useSaveGoodMutation();
+    const state = useSelector(state => state);
+    let currentCategory = getCurrentEntity(frontEndNames.category, state)
+
+    let isAdmin = isCurrentUserAdmin(state);
+
+    if (!isLoading && !good && isAdmin) {
+        let categories = currentCategory ? [{ _id: currentCategory._id, name: currentCategory.name }] : [];
+        good = { _id: undefined, categories, images: [] };
+    }
+
+    return !isLoading &&
+        (isAdmin ? <EditableGood good={good} saveGood={saveGoodMutation} maxWidth={maxWidth} /> : <LackPermissions name="good" />)
+}
+
+
+export { CEditableGood }

+ 142 - 0
src/Components/EditableUser.js

@@ -0,0 +1,142 @@
+import { useSelector } from "react-redux"
+import { getCurrentUser, useSaveUserMutation, useUserFindQuery } from "../reducers";
+import { useParams } from "react-router-dom";
+import { Button, Card, CardActions, CardContent, CardMedia, Checkbox, Container, FormControlLabel, FormGroup, Grid, InputAdornment, TextField, Typography } from "@mui/material";
+import { useEffect, useState } from "react";
+import { getFullImageUrl, saveImage } from "../utills/utils";
+import { UserEntity } from "../Entities";
+
+const EditableUser = ({ user: userExt = {_id: null, login: '', nick: ''}, maxWidth = 'md', saveUser, isAdminPermissions }) => {
+    const copyUser = user => new UserEntity(user);
+
+    let [user, setUser] = useState(copyUser(userExt));
+
+    useEffect(() => {
+        setUser(copyUser(userExt));
+    }, [userExt]);
+
+    const setUserData = (data) => {
+        let userData = copyUser({ ...user, ...data });
+        setUser(userData);
+        return userData;
+    }
+    const saveFullUser = async () => {
+        saveUser({ user: { _id: user._id, nick: user.nick, acl: user.acl ?? [] } });
+    }
+
+    const uploadAvatar = async param => {
+        let image = await saveImage({ data: param.target.files[0] }, false);
+        let userToSave = { _id: user._id, avatar: { _id: image._id } };
+        saveUser({ user: userToSave });
+    }
+
+    return user && (
+        <>
+            <Container maxWidth={maxWidth}>
+                <Card variant='outlined'>
+                    <Grid container spacing={maxWidth === 'xs' ? 7 : 5} rowSpacing={2}>
+                        <Grid item xs={12}>
+                            <Grid container spacing={2}>
+                                <Grid item xs={4}>
+                                    <CardMedia
+                                        component="img"
+                                        sx={{ height: 300, padding: "1em 1em 0 1em", objectFit: "contain" }}
+                                        image={getFullImageUrl(user.avatar)}
+                                        title={user.name}
+                                    />
+                                    <Button
+                                        variant="contained"
+                                        component="label"
+                                    >
+                                        Upload File
+                                        <input
+                                            type="file"
+                                            hidden
+                                            onChange={param => uploadAvatar(param)}
+                                        />
+                                    </Button>
+                                </Grid>
+                                <Grid item xs={8}>
+                                    <CardContent>
+                                        <Grid container rowSpacing={2}>
+                                            <Grid item width="100%">
+                                                <TextField
+                                                    required
+                                                    id="outlined-required"
+                                                    label="Nick"
+                                                    value={user.nick}
+                                                    onChange={event => setUserData({ nick: event.target.value })}
+                                                    fullWidth
+                                                />
+                                            </Grid>
+                                            <Grid item width="100%">
+                                                <TextField
+                                                    required
+                                                    id="outlined-required"
+                                                    label="Login"
+                                                    startAdornment={<InputAdornment position="start">$</InputAdornment>}
+                                                    value={user.login}
+                                                    onChange={event => setUserData({ login: event.target.value })}
+                                                    fullWidth
+                                                />
+                                            </Grid>
+                                            <Grid item width="100%">
+                                                <FormGroup>
+                                                    <FormControlLabel control={(
+                                                        <Checkbox
+                                                            checked={user.isUserRole}
+                                                            disabled={!isAdminPermissions}
+                                                            onChange={e => { user.setUserRole(e.target.checked); setUser(copyUser(user)); }}
+                                                        />)} label="User" />
+                                                    <FormControlLabel control={(
+                                                        <Checkbox
+                                                            checked={user.isAdminRole}
+                                                            disabled={!isAdminPermissions}
+                                                            onChange={e => { user.setAdminRole(e.target.checked); setUser(copyUser(user)); }}
+                                                        />)} label="Admin" />
+                                                </FormGroup>
+                                            </Grid>
+                                        </Grid>
+                                    </CardContent>
+                                </Grid>
+                            </Grid>
+                        </Grid>
+                    </Grid>
+                    <CardActions>
+                        <Button size='small' color='primary'
+                            onClick={() => saveFullUser(user)}
+                        >
+                            Save
+                        </Button>
+                        <Button size='small' color='primary'
+                            onClick={() => setUser(copyUser(userExt))}
+                        >
+                            Cancel
+                        </Button>
+                    </CardActions>
+                </Card>
+            </Container >
+
+        </>
+    )
+}
+
+const CEditableUser = ({ maxWidth = 'md' }) => {
+    const { _id } = useParams();
+    let currentUser = useSelector(state => new UserEntity(getCurrentUser(state)));
+    const { isLoading, data } = useUserFindQuery(_id ?? currentUser?._id ?? 'jfbvwkbvjeb');
+    let user = isLoading ? undefined : data?.UserFindOne;
+    user = _id ? user : currentUser;
+    const [saveUserMutation, { }] = useSaveUserMutation();
+
+    let isCurrentUser = currentUser?._id === _id || !_id;
+    let isAdminPermissions = currentUser?.isAdminRole ?? false;
+
+
+    return user && (isAdminPermissions || isCurrentUser) ? (
+        <EditableUser user={user} maxWidth={maxWidth} isAdminPermissions={isAdminPermissions} saveUser={saveUserMutation} />) :
+        <Typography>Permission denied</Typography>;
+}
+
+
+export { CEditableUser }

+ 32 - 0
src/Components/FileDropZone.js

@@ -0,0 +1,32 @@
+import React, { useCallback, useState } from 'react'
+import { useDropzone } from 'react-dropzone'
+
+function FileDropZone({ onDropFiles }) {
+  const [paths, setPaths] = useState([]);
+  const onDrop = useCallback(acceptedFiles => {
+
+    acceptedFiles = acceptedFiles.map(f => {
+      let url = URL.createObjectURL(f)
+      return { _id: null, name: f.path, url, data: f }
+    }
+    );
+    setPaths(acceptedFiles);
+    onDropFiles(acceptedFiles);
+  }, [setPaths])
+  const { getRootProps, getInputProps } = useDropzone({ onDrop })
+  console.log(paths);
+  return (
+    <>
+      <div {...getRootProps()}>
+        <input {...getInputProps()} />
+        <div style={{backgroundColor: 'lightgrey', minHeight: '100px', margin:'auto', display: 'inline-block'}} >
+          {
+            <p>Drop the files here ...</p>
+        }
+        </div>
+      </div>
+    </>
+  )
+}
+
+export { FileDropZone }

+ 160 - 0
src/Components/Good.js

@@ -0,0 +1,160 @@
+import React, { useState } from 'react';
+import Button from '@mui/material/Button';
+import { styled } from '@mui/material/styles';
+import { Container, Typography, Grid, CardActionArea, Card, CardContent, CardMedia, AvatarGroup, CardActions, Collapse, IconButton } from '@mui/material';
+import { getFullImageUrl } from "./../utills";
+import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
+import { AvatarAnimated } from './AvatarAnimated';
+import { actionAddGoodToCart, isCurrentUserAdmin } from '../reducers';
+import { useDispatch, useSelector } from 'react-redux';
+import { useGetGoodByIdQuery } from '../reducers';
+import { useParams } from 'react-router-dom';
+import { MyLink } from './MyLink';
+import { ModalContainer } from './ModalContainer';
+import { CategoryBreadcrumbs } from './CategoryBreadcrumbs';
+import { LoadingState } from './LoadingState';
+
+
+export const ExpandMore = styled(props => {
+    const { expand, ...other } = props;
+    return <IconButton {...other} />;
+})(({ theme, expand }) => ({
+    transform: !expand ? 'rotate(0deg)' : 'rotate(180deg)',
+    marginLeft: 'auto',
+    transition: theme.transitions.create('transform', { duration: theme.transitions.duration.shortest })
+}))
+
+export const AvatarGroupOriented = styled((props) => {
+    const { vertical, ...other } = props;
+    return <AvatarGroup {...other} />;
+})(({ theme, vertical }) => (vertical && {
+    display: 'flex',
+    flexDirection: 'column',
+    marginLeft: 10,
+    '& >:first-child': {
+        marginTop: 10,
+    },
+    '& >*': {
+        marginLeft: 1,
+        marginTop: theme.spacing(1),
+    },
+    ".MuiAvatar-root": { marginLeft: 1 }
+}));
+
+
+const Good = ({ good, maxWidth = 'md', isAdmin, showAddToCard = true, actionAddGoodToCart, editable }) => {
+    let [currentImageIndex, setCurrentImageIndex] = useState(0);
+    let [expanded, setExpanded] = useState(true);
+    let [previewMedia, setPreviewMedia] = useState(false);
+    const handleExpandClick = () => setExpanded(!expanded);
+    const currentCategory = good?.categories?.length > 0 ? good.categories[0] : undefined;
+    return good && (
+        <>
+            {
+                previewMedia ?
+                    <ModalContainer onCloseClick={() => setPreviewMedia(false)}>
+                        <CardMedia
+                            component="img"
+                            sx={{ height: 300, padding: "1em 1em 0 1em", objectFit: "contain" }}
+                            image={good.images?.length > 0 ? getFullImageUrl(good.images[currentImageIndex]) : ''}
+                            title={good.name}
+                        />
+                    </ModalContainer>
+                    :
+                    <></>
+            }
+            <Container maxWidth={maxWidth}>
+                <CategoryBreadcrumbs category={currentCategory} showLeafAsLink={true} />
+                <Card variant='outlined'>
+                    <Grid container spacing={maxWidth === 'xs' ? 7 : 5}>
+                        <Grid item xs={1}>
+                            <AvatarGroupOriented variant='rounded' vertical>
+                                {
+                                    good.images?.map((image, index) => (
+                                        <AvatarAnimated selected={index === currentImageIndex} variant='rounded' key={index} src={getFullImageUrl(image)}
+                                            onClick={() => {
+                                                setCurrentImageIndex(index)
+                                            }} />
+                                    ))
+                                }
+                            </AvatarGroupOriented>
+                        </Grid>
+                        <Grid item xs>
+                            <CardActionArea>
+                                <Grid container spacing={2}>
+                                    <Grid item xs={4}>
+                                        <CardMedia
+                                            component="img"
+                                            sx={{ height: 300, padding: "1em 1em 0 1em", objectFit: "contain" }}
+                                            image={good.images?.length > 0 ? getFullImageUrl(good.images[currentImageIndex]) : ''}
+                                            title={good.name}
+                                            onClick={event => setPreviewMedia(true)}
+                                        />
+                                    </Grid>
+                                    <Grid item xs={8}>
+                                        <CardContent>
+                                            <Typography gutterBottom variant='h5' component='h2'>
+                                                {good.name}
+                                            </Typography>
+                                            <Typography gutterBottom variant='body2' color='textSecondary' component='p'>
+                                                {`Price: $${good.price}`}
+                                            </Typography>
+                                        </CardContent>
+                                    </Grid>
+                                </Grid>
+                            </CardActionArea>
+                        </Grid>
+                    </Grid>
+                    <CardActions>
+                        <ExpandMore
+                            expand={expanded}
+                            onClick={handleExpandClick}
+                            aria-expanded={expanded}
+                            aria-label='showMore'
+                        >
+                            <ExpandMoreIcon />
+                        </ExpandMore>
+                        {
+                            showAddToCard && (
+                                <Button size='small' color='primary'
+                                    onClick={actionAddGoodToCart}
+                                >
+                                    Add to cart
+                                </Button>
+                            )
+                        }
+                        {
+                            isAdmin && <MyLink to={`/editgood/${good._id}`}>
+                                {
+                                    editable && <Button size='small' color='primary'>
+                                        Edit
+                                    </Button>
+                                }
+                            </MyLink>
+                        }
+                    </CardActions>
+                    <Collapse in={expanded} timeout='auto' unmountOnExit>
+                        <Typography paragraph sx={{ marginLeft: 1 }}>
+                            Description:
+                        </Typography>
+                        <Typography paragraph align='justify' sx={{ marginLeft: 2, marginRight: 2 }}>
+                            {good.description}
+                        </Typography>
+                    </Collapse>
+                </Card>
+            </Container>
+        </>
+    )
+}
+
+const CGood = ({ good, maxWidth = 'md', showAddToCard = true, editable = true }) => {
+    const { _id } = useParams();
+    const { isLoading, data } = useGetGoodByIdQuery(_id);
+    const dispatch = useDispatch();
+    if (!good)
+        good = isLoading ? { name: 'loading', goods: [] } : data?.GoodFindOne;
+    let state = useSelector(state => state);
+    let isAdmin = isCurrentUserAdmin(state);
+    return isLoading ? <LoadingState /> : <Good good={good} isAdmin={isAdmin} maxWidth={maxWidth} showAddToCard={showAddToCard} editable={editable} actionAddGoodToCart={() => dispatch(actionAddGoodToCart(good))} />
+}
+export { CGood };

+ 90 - 0
src/Components/GoodItem.js

@@ -0,0 +1,90 @@
+import React, { useState } from 'react';
+import Button from '@mui/material/Button';
+import { Container, Typography, Grid, CardActionArea, Card, CardContent, CardMedia, CardActions, Collapse } from '@mui/material';
+import { getFullImageUrl } from "./../utills";
+import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
+import { AvatarAnimated } from './AvatarAnimated';
+import { MyLink } from './MyLink';
+import { AvatarGroupOriented, ExpandMore } from './Good';
+import { useDispatch } from 'react-redux';
+import { actionAddGoodToCart } from '../reducers';
+
+export const GoodItem = ({ good, maxWidth = 'md', showAddToCard = true, actionAddGoodToCart }) => {
+    let [currentImageIndex, setCurrentImageIndex] = useState(0);
+    let [expanded, setExpanded] = useState(false);
+    const handleExpandClick = () => setExpanded(!expanded);
+    return good && (
+        <Container maxWidth={maxWidth}>
+            <Card variant='outlined'>
+                <Grid container spacing={maxWidth === 'xs' ? 7 : 5}>
+                    <Grid item xs={1}>
+                        <AvatarGroupOriented variant='rounded' vertical>
+                            {good.images?.map((image, index) => (
+                                <AvatarAnimated selected={index === currentImageIndex} variant='rounded' key={index} src={getFullImageUrl(image)}
+                                    onClick={() => {
+                                        setCurrentImageIndex(index);
+                                    }} />
+                            ))}
+                        </AvatarGroupOriented>
+                    </Grid>
+                    <Grid item xs>
+                        <MyLink to={`/good/${good._id}`}>
+                            <CardActionArea>
+                                <Grid container spacing={2}>
+                                    <Grid item xs={4}>
+                                        <CardMedia
+                                            component="img"
+                                            sx={{ height: 300, padding: "1em 1em 0 1em", objectFit: "contain" }}
+                                            image={good.images?.length > 0 ? getFullImageUrl(good.images[currentImageIndex]) : ''}
+                                            title={good.name} />
+                                    </Grid>
+                                    <Grid item xs={8}>
+                                        <CardContent>
+                                            <Typography gutterBottom variant='h5' component='h2'>
+                                                {good.name}
+                                            </Typography>
+                                            <Typography gutterBottom variant='body2' color='textSecondary' component='p'>
+                                                {`Price: $${good.price}`}
+                                            </Typography>
+                                        </CardContent>
+                                    </Grid>
+                                </Grid>
+                            </CardActionArea>
+                        </MyLink>
+                    </Grid>
+                </Grid>
+                <CardActions>
+                    <ExpandMore
+                        expand={expanded}
+                        onClick={handleExpandClick}
+                        aria-expanded={expanded}
+                        aria-label='showMore'
+                    >
+                        <ExpandMoreIcon />
+                    </ExpandMore>
+                    {showAddToCard && (
+                        <Button size='small' color='primary'
+                            onClick={actionAddGoodToCart}
+                        >
+                            Add to cart
+                        </Button>
+                    )}
+                </CardActions>
+                <Collapse in={expanded} timeout='auto' unmountOnExit>
+                    <Typography paragraph sx={{ marginLeft: 1 }}>
+                        Description:
+                    </Typography>
+                    <Typography paragraph align='justify' sx={{ marginLeft: 2, marginRight: 2 }}>
+                        {good.description}
+                    </Typography>
+                </Collapse>
+            </Card>
+        </Container>
+    );
+};
+
+const CGoodItem = ({ good, maxWidth = 'md', showAddToCard = true }) => {
+    const dispatch = useDispatch();
+    return <GoodItem good={good} maxWidth={maxWidth} showAddToCard={showAddToCard} actionAddGoodToCart={() => dispatch(actionAddGoodToCart(good))} />
+}
+export { CGoodItem };

+ 43 - 0
src/Components/GoodsList.js

@@ -0,0 +1,43 @@
+import React from 'react';
+import { Container, Box } from '@mui/material';
+import { CGoodItem } from './GoodItem';
+import { useSelector } from 'react-redux';
+import { useGetGoodsCountQuery, useGetGoodsQuery } from '../reducers';
+import { CSearchInput } from './SearchInput';
+import { CPagination } from './Pagination';
+import { frontEndNames, getCurrentEntity, getEntitiesListShowParams } from '../reducers/frontEndReducer';
+
+const GoodsList = ({  entities, entitiesTypeName }) => {
+    return (
+        <Container maxWidth='lg'>
+            <CSearchInput entitiesTypeName={entitiesTypeName} />
+            <Box sx={{ display: 'flex', flexWrap: 'wrap' }}>
+                {
+                    entities?.map(good => {
+                        return (
+                            <CGoodItem key={good._id} good={good} maxWidth='xs' />
+                        )
+                    })}
+            </Box>
+            <CPagination entitiesTypeName={entitiesTypeName} />
+        </Container>
+    )
+}
+
+const CGoodsList = () => {
+    let entitiesTypeName = frontEndNames.goods;
+    let state = useSelector(state => state);
+    const currentCategory = getCurrentEntity(frontEndNames.category, state);
+    const { fromPage, pageSize, searchStr } = getEntitiesListShowParams(entitiesTypeName, state);
+
+    let categoryFilter = currentCategory ? { "categories._id": currentCategory._id } : {};
+    const goodsResult = useGetGoodsQuery({ fromPage, pageSize, searchStr, queryExt: categoryFilter });
+    const goodsCountResult = useGetGoodsCountQuery({ searchStr, queryExt: categoryFilter });
+    let isLoading = goodsResult.isLoading || goodsCountResult.isLoading;
+
+    let entities = goodsResult.data?.GoodFind;
+    return !isLoading && entities && <GoodsList entitiesTypeName={entitiesTypeName} entities={entities} />
+}
+
+
+export { CGoodsList };

+ 9 - 0
src/Components/LackPermissions.js

@@ -0,0 +1,9 @@
+import { Typography } from "@mui/material"
+
+export const LackPermissions = ({name = '', method = 'view'}) => {
+    return (
+        <>
+            <Typography component="h3" variant="h3">{`Insuffcient permissions to ${method} ${name}`}</Typography>
+        </>
+    )
+}

+ 5 - 0
src/Components/LoadingState.js

@@ -0,0 +1,5 @@
+import { Typography } from "@mui/material";
+
+export const LoadingState = () =>{
+    return <Typography>Loading</Typography>;
+}

+ 87 - 0
src/Components/LoginForm.js

@@ -0,0 +1,87 @@
+import React, { useState } from 'react';
+import LockOutlinedIcon from '@mui/icons-material/LockOutlined';
+import { Container, CssBaseline, TextField, Avatar, Typography, FormControlLabel, Checkbox, Grid, Link, Button } from '@mui/material';
+import { Box } from '@mui/system';
+import { connect, useDispatch } from 'react-redux';
+import { MyLink } from './MyLink';
+import { actionAboutMe, useLoginMutation } from '../reducers/authReducer';
+
+const LoginForm = () => {
+    const [onLogin, { data, isLoading }] = useLoginMutation()
+    const dispatch = useDispatch()
+
+    const [login, setLogin] = useState('');
+    const [password, setPassword] = useState('');
+    const isButtonActive = !isLoading && login?.length > 3 && password?.length > 3;
+    return (
+        <Container component="main" maxWidth="xs">
+            <CssBaseline />
+            <Box
+                sx={{
+                    marginTop: 8,
+                    display: 'flex',
+                    flexDirection: 'column',
+                    alignItems: 'center',
+                }}
+            >
+                <Avatar sx={{ m: 1, bgcolor: 'secondary.main' }}>
+                    <LockOutlinedIcon />
+                </Avatar>
+                <Typography component="h1" variant="h5">
+                    Sign in
+                </Typography>
+                <TextField
+                    fullWidth
+                    margin="normal"
+                    autoFocus
+                    required
+                    id="login-input"
+                    label="Login"
+                    size="small"
+                    defaultValue=""
+                    onChange={e => setLogin(e.target.value)}
+                />
+                <TextField
+                    fullWidth
+                    margin="normal"
+                    required
+                    id="password-input"
+                    label="Password"
+                    type="password"
+                    size="small"
+                    defaultValue=""
+                    onChange={e => setPassword(e.target.value)}
+                />
+                <FormControlLabel
+                    control={<Checkbox value="remember" color="primary" />}
+                    label="Remember me"
+                />
+                <Button
+                    component="button"
+                    variant="contained"
+                    sx={{ mt: 3, mb: 2 }}
+                    fullWidth
+                    type="submit"
+                    disabled={!isButtonActive}
+                    onClick={() => onLogin({ login, password }).then(() => dispatch(actionAboutMe()))}>
+                    Login...
+                </Button>
+                <Grid container>
+                    <Grid item xs>
+                        <Link href="#" variant='body2'>
+                            Forgot password?
+                        </Link>
+                    </Grid>
+                    <Grid item>
+                        <MyLink to="/register" variant='body2'>
+                            {"Don't have an account? Sign Up"}
+                        </MyLink>
+                    </Grid>
+                </Grid>
+            </Box>
+        </Container>
+    )
+}
+const CLoginForm = connect(null, {})(LoginForm)
+
+export { CLoginForm };

+ 17 - 0
src/Components/Logout.js

@@ -0,0 +1,17 @@
+import { actionAuthLogout } from '../reducers';
+import { useEffect } from 'react';
+import { connect, useDispatch } from 'react-redux';
+import { history } from "../App";
+
+
+const Logout = () => {
+    const dispatch = useDispatch()
+    useEffect(() => {
+        dispatch(actionAuthLogout());
+        history.push('/');
+    }, []);
+
+    return <div></div>;
+};
+
+export const CLogout = connect(null, { onLogout: actionAuthLogout })(Logout)

+ 85 - 0
src/Components/MainAppBar.js

@@ -0,0 +1,85 @@
+import AppBar from '@mui/material/AppBar';
+import Box from '@mui/material/Box';
+import Toolbar from '@mui/material/Toolbar';
+import Typography from '@mui/material/Typography';
+import Button from '@mui/material/Button';
+import IconButton from '@mui/material/IconButton';
+import MenuIcon from '@mui/icons-material/Menu';
+import { MyLink } from './MyLink';
+import { connect, useSelector } from 'react-redux';
+import { useTheme } from '@emotion/react';
+import { actionSetSidebar, getCartItemsCount, getIsSideBarOpen } from '../reducers';
+import { UserEntity } from '../Entities';
+import ShoppingCartIcon from '@mui/icons-material/ShoppingCart';
+import { AccountCircle } from '@mui/icons-material';
+import LogoutIcon from '@mui/icons-material/Logout';
+import LoginIcon from '@mui/icons-material/Login';
+import CategoryIcon from '@mui/icons-material/Category';
+import SupervisedUserCircleIcon from '@mui/icons-material/SupervisedUserCircle';
+import WorkHistoryIcon from '@mui/icons-material/WorkHistory';
+import { Badge, Tooltip } from '@mui/material';
+import logo from '../images/logo.jpg';
+import AccountTreeIcon from '@mui/icons-material/AccountTree';
+
+const MainAppBar = ({ token, openSidebar }) => {
+    const cartItemsCount = useSelector(state => getCartItemsCount(state) ?? 0);
+    let currentUser = useSelector(state => new UserEntity(state.auth?.currentUser ?? { _id: null }));
+    let isAdmin = currentUser?.isAdminRole === true;
+    let isLoggedIn = token && true;
+    return (
+        <Box sx={{ flexGrow: 1 }}>
+            <AppBar position="static">
+                <Toolbar>
+                    <MyLink to="/">
+                        <Box component="img" src={logo} sx={{ width: "40px", borderStyle: "double", borderWidth: "3px", borderColor: "white", marginRight: "20px" }} />
+                    </MyLink>
+                    <IconButton
+                        size="large"
+                        edge="start"
+                        color="inherit"
+                        aria-label="menu"
+                        onClick={() => openSidebar(true)}
+                        sx={{ mr: 2 }}
+                        id="burger"
+                    >
+                        <MenuIcon />
+                    </IconButton>
+                    <Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
+                    </Typography>
+                    {
+                        !isLoggedIn &&
+                        <>
+                            <MyLink to="/login"><Button sx={{ color: "white" }}><Tooltip title="Login"><LoginIcon /></Tooltip></Button></MyLink>
+                            <MyLink to="/register"><Button sx={{ color: "white" }}>Register</Button></MyLink>
+                        </>
+                    }
+                    {
+                        isLoggedIn &&
+                        <>
+                            {isAdmin && (
+                                <>
+                                    <MyLink to="/categories"><Button sx={{ color: "white" }}><Tooltip title="Categories"><CategoryIcon /></Tooltip></Button></MyLink>
+                                    <MyLink to="/catree"><Button sx={{ color: "white" }}><Tooltip title="Categories Tree"><AccountTreeIcon/></Tooltip></Button></MyLink>
+                                    <MyLink to="/users"><Button sx={{ color: "white" }}><Tooltip title="Users"><SupervisedUserCircleIcon /></Tooltip></Button></MyLink>
+                                </>
+                            )}
+                            <MyLink to="/orders"><Button sx={{ color: "white" }}><Tooltip title="Orders"><WorkHistoryIcon /></Tooltip></Button></MyLink>
+                            <MyLink to="/user"><Button sx={{ color: "white" }}><Tooltip title="About Me"><AccountCircle /></Tooltip></Button></MyLink>
+                        </>
+                    }
+                    <Badge badgeContent={cartItemsCount} color="secondary">
+                        <MyLink to="/cart"><Button sx={{ color: "white" }}><Tooltip title="Cart"><ShoppingCartIcon /></Tooltip></Button></MyLink>
+                    </Badge>
+                    {
+                        isLoggedIn &&
+                        <>
+                            <MyLink to="/logout"><Button sx={{ color: "white" }}><Tooltip title="Logout"><LogoutIcon /></Tooltip></Button></MyLink>
+                        </>
+                    }
+                </Toolbar>
+            </AppBar>
+        </Box>
+    );
+}
+
+export const CMainAppBar = connect(state => ({ token: state.auth?.token, sidebarOpened: getIsSideBarOpen(state) }), { openSidebar: actionSetSidebar })(MainAppBar);

+ 43 - 0
src/Components/ModalContainer.js

@@ -0,0 +1,43 @@
+import { styled } from "@mui/material";
+
+const ModalOverlay = styled('div')({
+  position: "absolute",
+  zIndex: 9,
+  top: 0,
+  right: 0,
+  bottom: 0,
+  left: 0,
+  backgroundColor: "rgba(0, 0, 0, 0.7)",
+  display: "flex",
+  justifyContent: "center",
+  alignItems: "center",
+});
+
+const Modal = styled('div')({
+  backgroundColor: "#ffffff",
+  border: "1px solid #bebebe",
+  borderRadius: "2px",
+  padding: "12px 16px",
+  position: "relative",
+});
+
+const ModalClose = styled('div')({
+  position: "absolute",
+  right: "8px",
+  top: "4px",
+  fontSize: "24p",
+  cursor: "pointer"
+});
+
+const ModalContainer = ({ children, onCloseClick }) => 
+  <ModalOverlay>
+    <Modal>
+      <ModalClose onClick={onCloseClick}>
+        &#10005; {}
+      </ModalClose>
+      {children}
+    </Modal>
+  </ModalOverlay>
+
+
+export { ModalContainer };

+ 12 - 0
src/Components/MyLink.js

@@ -0,0 +1,12 @@
+import React, { useState, useEffect } from 'react';
+import { Link } from 'react-router-dom';
+import { history } from '../App';
+
+export const MyLink = ({ activeClassName = 'activeLink', className = '', to, ...props }) => {
+  const [currentPath, setCurrentPath] = useState(window.location.pathname);
+  useEffect(() => history.listen(({ pathname }) => setCurrentPath(pathname)),
+    []);
+  return (
+    <Link className={`${className} ${to === currentPath ? activeClassName : ''}`} to={to} {...props} />
+  );
+};

+ 56 - 0
src/Components/Order.js

@@ -0,0 +1,56 @@
+import React from 'react';
+import { Typography } from "@mui/material"
+import { Box, Container } from "@mui/system"
+import { useSelector } from "react-redux"
+import { useGetOrderByIdQuery } from "../reducers/ordersReducer"
+import { OrderGoodsList } from "./OrderGoodsList"
+import { useParams } from 'react-router-dom/cjs/react-router-dom';
+import { MyLink } from './MyLink';
+import { getCurrentUser } from '../reducers';
+import { UserEntity } from '../Entities';
+import { LackPermissions } from './LackPermissions';
+import { fixBackendDataError } from '../utills';
+import { LoadingState } from './LoadingState';
+
+const Order = ({ order = {} }) => {
+    return (
+        <>
+            <Container>
+                <Box>
+                    <Typography paragraph gutterBottom component={'h3'} variant={'h3'}>
+                        Order# {order._id}
+                    </Typography>
+                    <Typography gutterBottom variant='body2' color='textSecondary' component='p'>
+                        {`Created at: ${new Date(+order.createdAt).toLocaleString()}`}
+                    </Typography>
+                    <Typography paragraph gutterBottom component={'h4'} variant={'h4'}>
+                        {
+                            order.owner ?
+                                <MyLink to={`/user/${order.owner._id}`}>
+                                    Owner# {order.owner?.nick || order.owner?.login}
+                                </MyLink>
+                                :
+                                "No owner"
+                        }
+                    </Typography>
+                    <OrderGoodsList orderGoods={order?.orderGoods} />
+                </Box>
+            </Container>
+        </>
+    )
+}
+const COrder = () => {
+    const { _id } = useParams();
+    let currentUser = useSelector(state => getCurrentUser(state));
+    
+    let getOrderById= useGetOrderByIdQuery({ _id, owner: new UserEntity(currentUser) });
+    let order = getOrderById.isLoading ? { name: 'loading', order: {} } : fixBackendDataError(getOrderById, "OrderFindOne");
+    return !getOrderById.isLoading && 
+        order ?
+        <Order order={order} />
+        :
+        getOrderById.isLoading ? <LoadingState /> : <LackPermissions name="order"/>
+        ;
+}
+
+export { COrder };

+ 55 - 0
src/Components/OrderGood.js

@@ -0,0 +1,55 @@
+import { Typography } from "@mui/material";
+import { getFullImageUrl } from "./../utills";
+import { AvatarImage } from "./AvatarAnimated";
+import { StyledTableCell, StyledTableRow } from "./StyledTableElements";
+import "./orderGood.css"
+import { MyLink } from "./MyLink";
+
+const OrderGood = ({ orderGood, orderGoodNum }) => {
+    orderGood = { ...orderGood.good, ...orderGood };
+    return (
+        <>
+            <StyledTableRow>
+                <StyledTableCell item align="right" xs={1}>
+                    <Typography>
+                        {orderGoodNum + 1}.
+                    </Typography>
+                </StyledTableCell>
+                <StyledTableCell item xs={2}>
+                    {orderGood.images?.length > 0 ?
+                        <AvatarImage sx={{ width: 70, height: 70 }} variant='rounded' src={getFullImageUrl(orderGood.images[0])} /> :
+                        null}
+                </StyledTableCell>
+                <StyledTableCell item xs={3}>
+                    {orderGood?.good?._id ?
+                        <MyLink to={`/good/${orderGood?.good._id}`}>
+                            <Typography >
+                                {orderGood.name}
+                            </Typography>
+                        </MyLink>
+                        :
+                        <Typography >
+                            {orderGood.name}
+                        </Typography>
+                    }
+                </StyledTableCell>
+                <StyledTableCell item align="right" xs={2}>
+                    <Typography>
+                        {orderGood.price}
+                    </Typography>
+                </StyledTableCell>
+                <StyledTableCell item align="right" xs={2}>
+                    <Typography>
+                        {orderGood.count}
+                    </Typography>
+                </StyledTableCell>
+                <StyledTableCell item align="right" xs={2}>
+                    <Typography>
+                        {orderGood.price * orderGood.count}
+                    </Typography>
+                </StyledTableCell>
+            </StyledTableRow>
+        </>
+    )
+}
+export { OrderGood };

+ 61 - 0
src/Components/OrderGoodsList.js

@@ -0,0 +1,61 @@
+import React from 'react';
+import { Paper } from '@mui/material';
+import { OrderGood } from './OrderGood';
+import { Table, TableBody, TableContainer, TableHead, TableRow, TableCell } from "@mui/material";
+import { StyledTableCell } from './StyledTableElements';
+
+
+const OrderGoodsList = ({ orderGoods = [], tax_rate = 0 }) => {
+    function ccyFormat(num) {
+        return `${(num ?? 0).toFixed(2)}`;
+    }
+    function subtotal(items) {
+        return items?.map(({ price, count }) => price * count).reduce((sum, i) => sum + i, 0) ?? 0;
+    }
+    const invoiceSubtotal = subtotal(orderGoods);
+    const invoiceTaxes = tax_rate * invoiceSubtotal;
+    const invoiceTotal = invoiceTaxes + invoiceSubtotal;
+
+    return (
+        <>
+            <TableContainer component={Paper} sx={{ minWidth: 700, maxWidth: 1200 }} >
+                <Table aria-label="customized table">
+                    <TableHead>
+                        <TableRow>
+                            <StyledTableCell align="right">#</StyledTableCell>
+                            <StyledTableCell></StyledTableCell>
+                            <StyledTableCell>Name</StyledTableCell>
+                            <StyledTableCell align="right">Price ($)</StyledTableCell>
+                            <StyledTableCell align="right">Count</StyledTableCell>
+                            <StyledTableCell align="right">Total</StyledTableCell>
+                        </TableRow>
+                    </TableHead>
+                    <TableBody>
+                        {
+                            orderGoods?.map((orderGood, index) => {
+                                return (
+                                    <OrderGood key={orderGood._id} orderGood={orderGood} orderGoodNum={index} maxWidth='xs' />
+                                )
+                            })
+                        }
+                        <TableRow>
+                            <TableCell rowSpan={3} colSpan={3} />
+                            <TableCell colSpan={2}>Subtotal</TableCell>
+                            <TableCell align="right">{ccyFormat(invoiceSubtotal)}</TableCell>
+                        </TableRow>
+                        <TableRow>
+                            <TableCell>Tax</TableCell>
+                            <TableCell align="right">{`${(tax_rate * 100).toFixed(0)} %`}</TableCell>
+                            <TableCell align="right">{ccyFormat(invoiceTaxes)}</TableCell>
+                        </TableRow>
+                        <TableRow>
+                            <TableCell colSpan={2}>Total</TableCell>
+                            <TableCell align="right">{ccyFormat(invoiceTotal)}</TableCell>
+                        </TableRow>
+                    </TableBody>
+                </Table>
+            </TableContainer>
+        </>
+    )
+}
+export { OrderGoodsList };

+ 138 - 0
src/Components/OrderList.js

@@ -0,0 +1,138 @@
+import React from 'react';
+import { Container, Typography, Paper } from '@mui/material';
+import { Table, TableBody, TableContainer, TableHead, TableRow } from "@mui/material";
+import { StyledTableCell, StyledTableRow } from './StyledTableElements';
+import { CPagination } from './Pagination';
+import { CSearchInput } from './SearchInput';
+import { MyLink, ReferenceLink } from '.';
+import { useSelector } from 'react-redux';
+import { frontEndNames, getCurrentUser, getEntitiesListShowParams, useGetOrdersCountQuery, useGetOrdersQuery } from '../reducers';
+import { UserEntity } from '../Entities';
+import { fixBackendDataError } from '../utills';
+
+
+const OrderList = ({ entities, entitiesTypeName, fromPage, pageSize }) => {
+
+    let headCells = [
+        {
+            id: '#',
+            numeric: true,
+            disablePadding: true,
+            label: '#',
+            align: "center"
+        },
+        {
+            id: 'Date',
+            numeric: true,
+            disablePadding: true,
+            label: 'Date',
+        },
+        {
+            id: 'Order ID',
+            numeric: true,
+            disablePadding: true,
+            label: 'Order ID',
+        },
+        {
+            id: 'Total ($)',
+            numeric: true,
+            disablePadding: true,
+            label: 'Total ($)',
+            align: "right"
+        },
+        {
+            id: 'Owner',
+            numeric: true,
+            disablePadding: true,
+            label: 'Owner',
+            align: "right"
+        },
+        {
+            id: 'Note',
+            numeric: true,
+            disablePadding: true,
+            label: 'Note',
+            align: "right"
+        },
+    ]
+    return (
+        <>
+            <Container maxWidth="lg">
+                <CSearchInput entitiesTypeName={entitiesTypeName} />
+                <TableContainer component={Paper} >
+                    <Table sx={{ overflow: 'scroll' }} >
+                        <TableHead>
+                            <TableRow>
+                                {
+                                    headCells.map(headCell => {
+                                        return <StyledTableCell key={headCell.id} align={headCell.align}>{headCell.label}</StyledTableCell>
+                                    })
+                                }
+                            </TableRow>
+                        </TableHead>
+                        {entities?.length > 0 && (
+                            <TableBody>
+                                {
+                                    entities.map((order, index) => {
+                                        return (
+                                            <StyledTableRow key={order._id}>
+                                                <StyledTableCell align="right" >
+                                                    <Typography>
+                                                        {(fromPage * pageSize) + index + 1}.
+                                                    </Typography>
+                                                </StyledTableCell>
+                                                <StyledTableCell  >
+                                                    {new Date(+order.createdAt).toLocaleString()}
+                                                </StyledTableCell>
+                                                <StyledTableCell  >
+                                                    <MyLink to={`/order/${order._id}`}>
+                                                        <Typography >
+                                                            {order._id}
+                                                        </Typography>
+                                                    </MyLink>
+                                                </StyledTableCell>
+                                                <StyledTableCell align="right" >
+                                                    <Typography >
+                                                        {order.total}
+                                                    </Typography>
+                                                </StyledTableCell>
+                                                <StyledTableCell align="right" >
+                                                    {
+                                                        <ReferenceLink entity={order} refName='owner' typeName={frontEndNames.users} getText={ref => ref ? ref.nick || ref.login : "No owner"} />
+                                                    }
+                                                </StyledTableCell>
+                                                <StyledTableCell align="right" >
+                                                    <Typography>
+                                                        {order.notes}
+                                                    </Typography>
+                                                </StyledTableCell>
+                                            </StyledTableRow>
+                                        )
+                                    })
+                                }
+                            </TableBody>
+                        )}
+                    </Table>
+                    <CPagination entitiesTypeName={entitiesTypeName} />
+                </TableContainer>
+            </Container>
+        </>
+    )
+
+}
+const COrdersList = () => {
+    let entitiesTypeName = frontEndNames.orders;
+    let state = useSelector(state => state);
+    const { fromPage, pageSize, searchStr } = getEntitiesListShowParams(entitiesTypeName, state);
+    let currentUser = useSelector(state => new UserEntity(getCurrentUser(state)));
+
+    const ordersResult = useGetOrdersQuery({ fromPage, pageSize, searchStr, owner: currentUser });
+    const ordersCountResult = useGetOrdersCountQuery({ searchStr, owner: currentUser });
+    let isLoading = ordersResult.isLoading || ordersCountResult.isLoading;
+
+    let entities = !isLoading && fixBackendDataError(ordersResult, "OrderFind");
+    return !isLoading && <OrderList entities={entities} entitiesTypeName={entitiesTypeName} fromPage={fromPage} pageSize={pageSize} />
+}
+
+
+export { COrdersList };

+ 42 - 0
src/Components/Pagination.js

@@ -0,0 +1,42 @@
+import { TablePagination } from '@mui/material';
+import { useDispatch, useSelector } from 'react-redux';
+import { actionSetPaging, getEntitiesListShowParams } from '../reducers';
+import { getEntitiesCount } from '../reducers';
+
+const Pagination = ({ allEntitiesCount, fromPage, pageSize, changePageFE, changeRowsPerPageFE }) => {
+    allEntitiesCount = allEntitiesCount ?? 0;
+    const handleChangePage = (event, newPage) => {
+        changePageFE(newPage);
+    };
+    const handleChangeRowsPerPage = (event) => {
+        let newPageSize = parseInt(event.target.value, 10);
+        changeRowsPerPageFE(newPageSize);
+    };
+    return (
+        <TablePagination
+            rowsPerPageOptions={[5, 10, 25]}
+            component="div"
+            count={allEntitiesCount}
+            rowsPerPage={pageSize}
+            page={fromPage}
+            onPageChange={handleChangePage}
+            onRowsPerPageChange={handleChangeRowsPerPage}
+        />
+    )
+}
+
+
+export const CPagination = ({entitiesTypeName}) => {
+    const setPaging = (paging) => actionSetPaging(entitiesTypeName, paging)
+    let state = useSelector(state => state);
+    let allEntitiesCount = getEntitiesCount(entitiesTypeName, state);
+    let dispatch = useDispatch();
+    let changePageFE = (fromPage) =>
+    {
+        dispatch(setPaging({ fromPage }));
+    }
+    let changeRowsPerPageFE = pageSize => 
+        dispatch(setPaging({ fromPage: 0, pageSize }));
+    let {fromPage, pageSize} = getEntitiesListShowParams(entitiesTypeName, state);
+    return <Pagination allEntitiesCount={allEntitiesCount} fromPage={fromPage} pageSize={pageSize} changePageFE={changePageFE} changeRowsPerPageFE={changeRowsPerPageFE} />
+}

+ 37 - 0
src/Components/ReferenceLink.js

@@ -0,0 +1,37 @@
+import { MyLink } from "./MyLink";
+import { Typography } from '@mui/material';
+import { frontEndNames, isCurrentUserAdmin } from "../reducers";
+import { useSelector } from "react-redux";
+
+export const ReferenceLink = ({ entity, refName, getText, path, typeName }) => {
+    let state = useSelector(state => state);
+    let isAdmin = isCurrentUserAdmin(state);
+    let refEntity = { ...(refName ? entity[refName] : entity) };
+    
+
+    if (!path) {
+        if (typeName === frontEndNames.users) {
+            path = 'user';
+            if (!isAdmin) {
+                path = "user";
+                refEntity._id = undefined;
+            }
+        }
+        if (typeName === frontEndNames.category)
+            path = "category";
+        if (typeName === frontEndNames.orders)
+            path = "order";
+        if (typeName === frontEndNames.goods)
+            path = isAdmin ? 'editablegood' : "good";
+    }
+    return (
+        refEntity ?
+            <MyLink to={`/${path}/${refEntity._id}`}>
+                <Typography>
+                    {getText(refEntity)}
+                </Typography>
+            </MyLink>
+            :
+            (<Typography>{getText(refEntity)}</Typography>)
+    );
+}

+ 109 - 0
src/Components/RegisterForm.js

@@ -0,0 +1,109 @@
+import React, { useState } from 'react';
+import LockOutlinedIcon from '@mui/icons-material/LockOutlined';
+import { Container, CssBaseline, TextField, Avatar, Typography, FormControlLabel, Checkbox, Button } from '@mui/material';
+import { Box } from '@mui/system';
+import { connect, useDispatch } from 'react-redux';
+import { MyLink } from './MyLink';
+import { actionAboutMe, useLoginMutation, useRegisterMutation } from '../reducers/authReducer';
+
+const RegisterForm = () => {
+    const [onRegister, { isLoading: isLoadingReg }] = useRegisterMutation();
+
+    const [onLogin, { }] = useLoginMutation();
+    const dispatch = useDispatch()
+
+    const [login, setLogin] = useState('');
+    const [nick, setNick] = useState('');
+    const [password, setPassword] = useState('');
+    const [passwordRetype, setPasswordRetype] = useState('');
+    const arePasswordsEqual = password === passwordRetype;
+    const isButtonActive = !isLoadingReg && arePasswordsEqual && login?.length > 3 && password?.length > 3;
+    return (
+        <Container component="main" maxWidth="xs">
+            <CssBaseline />
+            <Box
+                sx={{
+                    marginTop: 8,
+                    display: 'flex',
+                    flexDirection: 'column',
+                    alignItems: 'center',
+                }}
+            >
+                <Avatar sx={{ m: 1, bgcolor: 'secondary.main' }}>
+                    <LockOutlinedIcon />
+                </Avatar>
+                <Typography component="h1" variant="h5">
+                    Sign up
+                </Typography>
+                <TextField
+                    fullWidth
+                    margin="normal"
+                    autoFocus
+                    required
+                    id="register-input"
+                    label="Nick"
+                    size="small"
+                    defaultValue=""
+                    onChange={e => setNick(e.target.value)}
+                />
+                <TextField
+                    fullWidth
+                    margin="normal"
+                    autoFocus
+                    required
+                    id="register-input"
+                    label="Login"
+                    size="small"
+                    defaultValue=""
+                    onChange={e => setLogin(e.target.value)}
+                />
+
+                <TextField
+                    fullWidth
+                    margin="normal"
+                    required
+                    id="password-input"
+                    label="Password"
+                    type="password"
+                    size="small"
+                    defaultValue=""
+                    onChange={e => setPassword(e.target.value)}
+                />
+                <TextField
+                    fullWidth
+                    margin="normal"
+                    required
+                    id="password-retype-input"
+                    label="Retype Password"
+                    type="password"
+                    size="small"
+                    defaultValue=""
+                    onChange={e => setPasswordRetype(e.target.value)}
+                    color={arePasswordsEqual ? 'primary' : 'error'}
+                />
+                <FormControlLabel
+                    control={<Checkbox value="remember" color="primary" />}
+                    label="Remember me"
+                />
+                <Button
+                    component="button"
+                    variant="contained"
+                    sx={{ mt: 3, mb: 2 }}
+                    fullWidth
+                    type="submit"
+                    disabled={!isButtonActive}
+                    onClick={() => (
+                        onRegister({ login, password, nick })
+                            .then(() => onLogin({ login, password }))
+                            .then(() => dispatch(actionAboutMe()))
+                    )}>
+                    Register...
+                </Button>
+            </Box>
+        </Container >
+    )
+}
+const CRegisterForm = connect(null, {})(RegisterForm)
+
+
+export { CRegisterForm };

+ 64 - 0
src/Components/RootCats.js

@@ -0,0 +1,64 @@
+import React from "react";
+import { List, ListItem, ListItemButton, ListItemText } from "@mui/material";
+import { MyLink } from ".";
+import { useGetRootCategoriesQuery } from "../reducers";
+import { AvatarImage } from "./AvatarAnimated";
+import ListItemAvatar from '@mui/material/ListItemAvatar';
+import CategoryIcon from '@mui/icons-material/Category';
+import { getFullImageUrl } from "../utills";
+
+export const CatsList = ({ cats = [] }) => {
+    const [selectedIndex, setSelectedIndex] = React.useState(-1);
+
+    const handleListItemClick = (event, index) => {
+        setSelectedIndex(index);
+    };
+    return (
+        <List sx={{ bgcolor: "" }}>
+            {cats && cats?.map((cat, index) => (
+                <CatItem cat={cat} key={cat._id} index={index} selectedIndex={selectedIndex} handleListItemClick={handleListItemClick} />
+            ))}
+        </List>
+    )
+};
+
+const CatItem = ({ cat, index, selectedIndex, handleListItemClick }) => {
+    return (
+        <MyLink to={`/category/${cat._id}`}>
+            <ListItemButton
+                selected={index === selectedIndex}
+                onClick={(event) => handleListItemClick(event, index)}
+            >
+                <ListItemAvatar>
+                    {
+                        cat.image?.url ?
+                            <AvatarImage variant='rounded' src={getFullImageUrl(cat.image)} />
+                            :
+                            <AvatarImage variant='rounded' sx={{ bgcolor: "rgba(184, 200, 239, 0.63)" }}><CategoryIcon color="secondary" /></AvatarImage>
+                    }
+                </ListItemAvatar>
+                <ListItemText
+                    primary={cat.name}
+                />
+            </ListItemButton>
+        </MyLink>
+    );
+    return (
+        <ListItem key={cat._id} disablePadding>
+            <ListItemButton>
+                <MyLink to={`/category/${cat._id}`}>
+                    <ListItemText primary={cat.name} />
+                </MyLink>
+            </ListItemButton>
+        </ListItem>
+    )
+};
+
+const CRootCats = () => {
+    const { isLoading, data } = useGetRootCategoriesQuery();
+    let cats = data?.CategoryFind;
+
+    return !isLoading && cats && <CatsList cats={cats} />
+}
+
+export { CRootCats };

+ 90 - 0
src/Components/SearchInput.js

@@ -0,0 +1,90 @@
+import * as React from 'react';
+import { styled, alpha } from '@mui/material/styles';
+import Box from '@mui/material/Box';
+import Toolbar from '@mui/material/Toolbar';
+import Typography from '@mui/material/Typography';
+import InputBase from '@mui/material/InputBase';
+import SearchIcon from '@mui/icons-material/Search';
+import { useDispatch } from 'react-redux';
+import { actionSetSearch } from '../reducers/frontEndReducer';
+import { useEffect } from 'react';
+
+const Search = styled('div')(({ theme }) => ({
+  position: 'relative',
+  borderRadius: theme.shape.borderRadius,
+  backgroundColor: alpha(theme.palette.common.white, 0.15),
+  '&:hover': {
+    backgroundColor: alpha(theme.palette.common.white, 0.25),
+  },
+  marginLeft: 0,
+  width: '100%',
+  [theme.breakpoints.up('sm')]: {
+    marginLeft: theme.spacing(1),
+    width: 'auto',
+  },
+}));
+
+const SearchIconWrapper = styled('div')(({ theme }) => ({
+  padding: theme.spacing(0, 2),
+  height: '100%',
+  position: 'absolute',
+  pointerEvents: 'none',
+  display: 'flex',
+  alignItems: 'center',
+  justifyContent: 'center',
+}));
+
+const StyledInputBase = styled(InputBase)(({ theme }) => ({
+  color: 'inherit',
+  '& .MuiInputBase-input': {
+    padding: theme.spacing(1, 1, 1, 0),
+    // vertical padding + font size from searchIcon
+    paddingLeft: `calc(1em + ${theme.spacing(4)})`,
+    transition: theme.transitions.create('width'),
+    width: '100%',
+    [theme.breakpoints.up('sm')]: {
+      width: '12ch',
+      '&:focus': {
+        width: '20ch',
+      },
+    },
+  },
+}));
+
+function SearchInput({ onChange, entitiesTypeName }) {
+  useEffect(() => {
+    return () => {
+      onChange(entitiesTypeName, '');
+    }
+  }, []);
+  return (
+    <Box sx={{ flexGrow: 1 }}>
+      <Toolbar>
+        <Typography
+          variant="h6"
+          noWrap
+          component="div"
+          sx={{ flexGrow: 1, display: { xs: 'none', sm: 'block' } }}
+        >
+        </Typography>
+        <Search>
+          <SearchIconWrapper>
+            <SearchIcon />
+          </SearchIconWrapper>
+          <StyledInputBase
+            placeholder="Search…"
+            inputProps={{ 'aria-label': 'search' }}
+            onChange={e => onChange(entitiesTypeName, e.target.value)}
+          />
+        </Search>
+      </Toolbar>
+    </Box>
+  );
+}
+
+
+
+export const CSearchInput = (props) => {
+  let dispatch = useDispatch();
+  return <SearchInput {...props} onChange={(entitiesTypeName, seacrhStr) => dispatch(actionSetSearch(entitiesTypeName, seacrhStr))} />
+}

+ 65 - 0
src/Components/Sidebar.js

@@ -0,0 +1,65 @@
+import React from 'react';
+import { styled, useTheme } from '@mui/material/styles';
+import Drawer from '@mui/material/Drawer';
+import Divider from '@mui/material/Divider';
+import IconButton from '@mui/material/IconButton';
+import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
+import ChevronRightIcon from '@mui/icons-material/ChevronRight';
+import { actionSetSidebar, getIsSideBarOpen } from '../reducers/frontEndReducer';
+import { connect } from 'react-redux';
+import { ClickAwayListener } from '@mui/material';
+
+function Sidebar(props) {
+    let { drawerWidth, menuComponent, opened, openSidebar } = props;
+    const [openedComp, setOpenedComp] = React.useState(undefined);
+    let MenuComponent = menuComponent;
+    drawerWidth = drawerWidth || 200;
+    const theme = useTheme();
+    const handleDrawerClose = () => {
+        openSidebar(false);
+        setOpenedComp(undefined)
+    };
+    const handleClickAwayListener = event => {
+        if (event.target.closest('#burger'))
+            return;
+        handleDrawerClose();
+    };
+    const DrawerHeader = styled('div')(({ theme }) => ({
+        display: 'flex',
+        alignItems: 'center',
+        padding: theme.spacing(0, 1),
+        // necessary for content to be below app bar
+        ...theme.mixins.toolbar,
+        justifyContent: 'flex-end',
+    }));
+
+    return (
+        <>
+            <ClickAwayListener onClickAway={event => handleClickAwayListener(event)}>
+                <Drawer
+                    sx={{
+                        width: drawerWidth,
+                        flexShrink: 0,
+                        '& .MuiDrawer-paper': {
+                            width: drawerWidth,
+                            boxSizing: 'border-box',
+                        },
+                    }}
+                    variant="persistent"
+                    anchor="left"
+                    open={opened}
+                >
+                    <DrawerHeader>
+                        <IconButton onClick={handleDrawerClose}>
+                            {theme.direction === 'ltr' ? <ChevronLeftIcon /> : <ChevronRightIcon />}
+                        </IconButton>
+                    </DrawerHeader>
+                    <Divider />
+                    <MenuComponent {...props} />
+                    <Divider />
+                </Drawer>
+            </ClickAwayListener>
+        </>);
+}
+
+export const CSidebar = connect(state => ({ opened: getIsSideBarOpen(state) }), { openSidebar: actionSetSidebar })(Sidebar);

+ 131 - 0
src/Components/SortedFileDropZone.js

@@ -0,0 +1,131 @@
+import { DndContext, KeyboardSensor, PointerSensor, useDroppable, useSensor, useSensors } from "@dnd-kit/core";
+import { rectSortingStrategy, SortableContext, sortableKeyboardCoordinates, useSortable } from "@dnd-kit/sortable";
+import { arrayMoveImmutable } from "array-move";
+import { useEffect, useState } from "react";
+import { CSS } from "@dnd-kit/utilities";
+import { FileDropZone } from "./FileDropZone";
+import { getFullImageUrl } from "../utills";
+import { CardMedia, Paper } from "@mui/material";
+
+const SortableItem = (props) => {
+    const {
+        attributes,
+        listeners,
+        setNodeRef,
+        transform,
+        transition
+    } = useSortable({ id: props.id });
+
+    const itemStyle = {
+        transform: CSS.Transform.toString(transform),
+        transition,
+        cursor: "grab",
+    };
+
+    const Render = props.render
+
+    return (
+        <div style={itemStyle} ref={setNodeRef} {...attributes} {...listeners}>
+            <Render {...{ item: props.item, ...(props.itemProp ?? {}) }} />
+        </div>
+    );
+};
+
+
+const Droppable = ({ id, items = [], itemProp, keyField, render }) => {
+    const { setNodeRef } = useDroppable({ id });
+
+    return (
+        <SortableContext id={id} items={items} strategy={rectSortingStrategy}>
+            {items.map((item) => (
+                <SortableItem render={render} key={item[keyField]} id={item}
+                    itemProp={itemProp} item={item} />
+            ))}
+        </SortableContext>
+    );
+};
+
+function CSortedFileDropZone(props) {
+    let render = file => {
+        file = file.item ?? file;
+        return (
+            <div>
+                <CardMedia component="img" key={file.name} src={file._id ? getFullImageUrl(file) : file.url} {...props.itemProp} />
+            </div>
+        );
+    }
+    props = { itemProp: { width: "100px" }, ...props, render: render, keyField: "name" }
+    return <SortedFileDropZone {...props} />
+}
+
+function SortedFileDropZone({ sx, items: startItems, render, itemProp, keyField, onChange, horizontal }) {
+    const [items, setItems] = useState(
+        startItems ?? []
+    );
+    useEffect(() => {
+        return setItems(startItems ?? []);
+    }
+        , [startItems])
+
+    useEffect(() => {
+        if (typeof onChange === 'function') {
+            onChange(items)
+        }
+    }, [items])
+
+    const sensors = useSensors(
+        useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
+        useSensor(KeyboardSensor, {
+            coordinateGetter: sortableKeyboardCoordinates
+        })
+    );
+
+    const handleDragEnd = ({ active, over }) => {
+        let activeIndex = active.data.current.sortable.index;
+        let overIndex = over?.data.current?.sortable.index;
+
+        setItems((items) => {
+            if (overIndex === undefined) {
+                if (items.length === 1)
+                    activeIndex = 0;
+                items = [...items]
+                items.splice(activeIndex, 1);
+                return items;
+            }
+            else
+                return arrayMoveImmutable(items, activeIndex, overIndex)
+        });
+    }
+
+    const onDropFiles = droppedFiles => {
+        return setItems(items => {
+            return [...items, ...droppedFiles]
+        }
+        );
+    }
+
+    const containerStyle = { display: horizontal ? "flex" : '' };
+
+    return (
+        <>
+            <Paper sx={sx}>
+                <FileDropZone onDropFiles={onDropFiles}>
+                </FileDropZone>
+                <DndContext
+                    sensors={sensors}
+                    onDragEnd={handleDragEnd}
+                >
+                    <div style={containerStyle}>
+                        <Droppable id="aaa"
+                            items={items}
+                            itemProp={itemProp}
+                            keyField={keyField}
+                            render={render} >
+                        </Droppable>
+                    </div>
+                </DndContext>
+            </Paper>
+        </>
+    );
+}
+export { CSortedFileDropZone };

+ 23 - 0
src/Components/StyledTableElements.js

@@ -0,0 +1,23 @@
+import { styled } from '@mui/material/styles';
+import { TableRow, TableCell, tableCellClasses } from "@mui/material";
+
+const StyledTableCell = styled(TableCell)(({ theme }) => ({
+    [`&.${tableCellClasses.head}`]: {
+        backgroundColor: theme.palette.common.black,
+        color: theme.palette.common.white,
+    },
+    [`&.${tableCellClasses.body}`]: {
+        fontSize: 14,
+    },
+}));
+const StyledTableRow = styled(TableRow)(({ theme }) => ({
+    '&:nth-of-type(odd)': {
+        backgroundColor: theme.palette.action.hover,
+    },
+    // hide last border
+    '&:last-child td, &:last-child th': {
+        border: 0,
+    },
+}));
+
+export {StyledTableCell, StyledTableRow}

+ 131 - 0
src/Components/UsersList.js

@@ -0,0 +1,131 @@
+import React from 'react';
+import { Container, Typography, Paper, Link } from '@mui/material';
+import { Table, TableBody, TableContainer, TableHead, TableRow } from "@mui/material";
+import { StyledTableCell, StyledTableRow } from './StyledTableElements';
+import { CPagination } from './Pagination';
+import { CSearchInput } from './SearchInput';
+import { MyLink } from '.';
+import { useSelector } from 'react-redux';
+import { frontEndNames, getEntitiesListShowParams, useGetUsersCountQuery, useGetUsersQuery } from '../reducers';
+
+const UsersList = ({ entities, fromPage, pageSize, entitiesTypeName }) => {
+
+    let headCells = [
+        {
+            id: '#',
+            numeric: true,
+            disablePadding: true,
+            label: '#',
+            align: "center",
+            xs: 1,
+        },
+        {
+            id: 'Date',
+            numeric: true,
+            disablePadding: true,
+            label: 'Date',
+            xs: 2,
+        },
+        {
+            id: 'User ID',
+            numeric: true,
+            disablePadding: true,
+            label: 'User ID',
+            xs: 3,
+        },
+        {
+            id: 'login',
+            numeric: true,
+            disablePadding: true,
+            label: 'login',
+            align: "right",
+            xs: 3,
+        },
+        {
+            id: 'Nick',
+            numeric: true,
+            disablePadding: true,
+            label: 'Nick',
+            align: "right",
+            xs: 3,
+        },
+    ]
+    return (
+        <>
+            <Container maxWidth="lg">
+                <CSearchInput entitiesTypeName={entitiesTypeName}/>
+                <TableContainer component={Paper} >
+                    <Table sx={{ overflow: 'scroll' }} >
+                        <TableHead>
+                            <TableRow>
+                                {
+                                    headCells.map(headCell => {
+                                        return <StyledTableCell key={headCell.id} align={headCell.align} xs={headCell.xs}>{headCell.label}</StyledTableCell>
+                                    })
+                                }
+                            </TableRow>
+                        </TableHead>
+                        {entities?.length > 0 && (
+                            <TableBody>
+                                {
+                                    entities.map((user, index) => {
+                                        return (
+                                            <StyledTableRow key={user._id}>
+                                                <StyledTableCell align="right" width="10%">
+                                                    <Typography>
+                                                        {(fromPage * pageSize) + index + 1}.
+                                                    </Typography>
+                                                </StyledTableCell>
+                                                <StyledTableCell width="15%">
+                                                    {new Date(+user.createdAt).toLocaleString()}
+                                                </StyledTableCell>
+                                                <StyledTableCell width="25%">
+                                                    <MyLink to={`/user/${user._id}`}>
+                                                        <Typography >
+                                                            {user._id}
+                                                        </Typography>
+                                                    </MyLink>
+                                                </StyledTableCell>
+                                                <StyledTableCell align="right" width="25%">
+                                                    <Typography >
+                                                        {user.login}
+                                                    </Typography>
+                                                </StyledTableCell>
+                                                <StyledTableCell align="right" width="25%">
+                                                    <Link href='#'>
+                                                        <Typography>
+                                                            {user.nick}
+                                                        </Typography>
+                                                    </Link>
+                                                </StyledTableCell>
+                                            </StyledTableRow>
+                                        )
+                                    })
+                                }
+                            </TableBody>
+                        )}
+                    </Table>
+                    <CPagination entitiesTypeName={entitiesTypeName} />
+                </TableContainer>
+            </Container>
+        </>
+    )
+}
+
+const CUsersList = () => {
+    let entitiesTypeName = frontEndNames.users;
+    let state = useSelector(state => state);
+    const { fromPage, pageSize, searchStr } = getEntitiesListShowParams(entitiesTypeName, state);
+
+    const usersResult = useGetUsersQuery({ fromPage, pageSize, searchStr });
+    const usersCountResult = useGetUsersCountQuery({ searchStr });
+    let isLoading = usersResult.isLoading || usersCountResult.isLoading;
+
+    let entities = !isLoading && usersResult.data?.UserFind;
+    return !isLoading && entities && <UsersList entities={entities} entitiesTypeName={entitiesTypeName} fromPage={fromPage} pageSize={pageSize} />
+}
+
+
+
+
+export { CUsersList };

+ 4 - 0
src/Components/cartGood.css

@@ -0,0 +1,4 @@
+.MuiAvatar-img{
+    object-fit: contain;
+  }
+  

+ 21 - 0
src/Components/index.js

@@ -0,0 +1,21 @@
+export { CLoginForm } from './LoginForm';
+export { CGoodItem } from './GoodItem';
+export { CGood } from './Good';
+export { CGoodsList } from './GoodsList';
+export { OrderGood } from './OrderGood';
+export { OrderGoodsList } from './OrderGoodsList'
+export { COrder } from './Order';
+export { COrdersList } from './OrderList';
+export { MyLink } from './MyLink';
+export { CLogout } from './Logout';
+export { CMainAppBar } from './MainAppBar';
+export { CRootCats } from './RootCats';
+export { CSortedFileDropZone } from './SortedFileDropZone';
+export { CEditableGood } from './EditableGood'
+export { CEditableUser as CUser } from './EditableUser';
+export { CUsersList } from './UsersList';
+export { CRegisterForm } from './RegisterForm';
+export { CCategoriesList } from './CategoryList';
+export { CCategoryTree } from './CategoryTree';
+export { ReferenceLink } from './ReferenceLink';
+export { CEditableCategory } from './EditableCategory';

+ 4 - 0
src/Components/orderGood.css

@@ -0,0 +1,4 @@
+.MuiAvatar-img{
+    object-fit: contain;
+  }
+  

+ 84 - 0
src/Entities/UserEntity.js

@@ -0,0 +1,84 @@
+import { arrayMoveImmutable } from "array-move";
+
+export class UserEntity {
+    #acl = [];
+    get acl() {
+        return [...this.#acl];
+    }
+    constructor(user) {
+        this._id = user._id;
+        this.nick = user.nick;
+        this.login = user.login;
+        this.avatar = user.avatar ? { ...user.avatar } : null;
+        this.#acl = [...(user.acl ?? [])];
+        this.#fixRoles();
+    }
+    #fixRoles = () => {
+        let _id = this._id;
+        let acl = this.#acl;
+        let onlyAllowedAcls = acl.filter(a => a === this._id || a === "user" || a === "admin");
+        if (onlyAllowedAcls.length !== acl.length) {
+            let uniqueRoles = new Set(onlyAllowedAcls);
+            if (uniqueRoles.length !== acl.length) {
+                this.#acl = acl = [...uniqueRoles];
+            }
+        }
+        if (_id) {
+            let myRoleIdx = this.#getRoleIdx(_id);
+            if (myRoleIdx < 0) {
+                acl.splice(0, 0, _id);
+            }
+            else if (myRoleIdx > 0) {
+                this.#acl = acl = arrayMoveImmutable(acl, myRoleIdx, 0);
+            }
+        }
+        let rolesCnt = this.acl.length;
+        let offset = _id ? 1 : 0;
+        if (rolesCnt === offset) {
+            acl.push("user")
+        }
+        else if (rolesCnt > offset && acl[offset] !== "user") {
+            if (rolesCnt === offset + 1) {
+                acl.splice(1, 0, "user")
+            }
+            else {
+                this.#acl = acl = arrayMoveImmutable(acl, offset, offset + 1);
+            }
+        }
+    }
+
+
+    #getRoleIdx = (role) => {
+        let res = this.#acl?.indexOf(role);
+        return res ?? -1;
+    }
+    #isRole = (role) => this.#getRoleIdx(role) >= 0;
+    get isAdminRole() { return this.#isRole("admin"); }
+    get isUserRole() {
+        return this.#isRole("user");
+    }
+    onSetRoleInt = () => {
+        this.onSetRole(this);
+        this.#fixRoles();
+    }
+    #setRole = (role, isSet, onSetRole = undefined) => {
+        this.#acl ??= [];
+        let roleIdx = this.#getRoleIdx(role);
+        if (isSet) {
+            if (roleIdx < 0) {
+                this.#acl.push(role);
+                if (this.onSetRole)
+                    this.onSetRoleInt();
+            }
+        }
+        else {
+            if (roleIdx >= 0) {
+                this.#acl.splice(roleIdx, 1);
+                if (onSetRole)
+                    this.onSetRoleInt();
+            }
+        }
+    }
+    setAdminRole = (isSet, onSetRole = undefined) => { this.#setRole("admin", isSet, onSetRole) };
+    setUserRole = (isSet, onSetRole = undefined) => { this.#setRole("user", isSet, onSetRole) };
+}

+ 1 - 0
src/Entities/index.js

@@ -0,0 +1 @@
+export { UserEntity } from "./UserEntity"

BIN
src/images/logo.jpg


BIN
src/images/theme_main.png


+ 14 - 0
src/index.css

@@ -0,0 +1,14 @@
+body {
+  margin: 0;
+  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
+    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
+    sans-serif;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+code {
+  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
+    monospace;
+}
+

+ 15 - 0
src/index.js

@@ -0,0 +1,15 @@
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import './index.css';
+import App from './App.js';
+import reportWebVitals from './reportWebVitals';
+
+const root = ReactDOM.createRoot(document.getElementById('root'));
+
+
+root.render(
+  <App />
+);
+
+reportWebVitals();
+

File diff suppressed because it is too large
+ 1 - 0
src/logo.svg


+ 160 - 0
src/reducers/authReducer.js

@@ -0,0 +1,160 @@
+import { gql } from "graphql-request";
+import { createApi } from '@reduxjs/toolkit/query/react'
+import { graphqlRequestBaseQuery } from '@rtk-query/graphql-request-base-query' //npm install
+import { getFullBackendUrl, jwtDecode } from "../utills";
+import { createSlice } from "@reduxjs/toolkit";
+import { history } from "../App";
+import { UserEntity } from "../Entities";
+import { createFullQuery } from "../utills";
+
+const getUsersSearchParams = (searchStr, queryExt) => (
+    {
+        searchStr: searchStr,
+        searchFieldNames: ["nick", "login"],
+        queryExt
+    });
+
+export const prepareHeaders = (headers, { getState }) => {
+    const token = getState().auth.token;
+    if (token) {
+        headers.set("Authorization", `Bearer ${token}`);
+    }
+    return headers;
+}
+
+const authApi = createApi({
+    reducerPath: "authApi",
+    baseQuery: graphqlRequestBaseQuery({
+        url: getFullBackendUrl('/graphql'),
+        prepareHeaders
+    }),
+    endpoints: (builder) => ({
+        login: builder.mutation({
+            query: ({ login, password }) => ({
+                document: gql`
+                    query login($login: String, $password: String) {
+                        login(login: $login, password: $password) 
+                    }
+                    `,
+                variables: { login, password }
+            })
+        }),
+        register: builder.mutation({
+            query: ({ login, password, nick }) => ({
+                document: gql`
+                mutation UserRegistration($login: String, $password: String, $nick: String) {
+                    UserUpsert(user: {login: $login, password: $password, nick: $nick}) {
+                      _id
+                      createdAt
+                      nick
+                      acl
+                    }
+                  }
+                    `,
+                variables: { login, password, nick: nick || login }
+            })
+        }),
+        userFind: builder.query({
+            query: (_id) => ({
+                document: gql`
+                    query UserFind($q: String) {
+                        UserFindOne(query: $q){
+                            _id login nick acl avatar {_id url} createdAt
+                        } 
+                    }
+                    `,
+                variables: { q: JSON.stringify([{ _id }]) }
+            }),
+            providesTags: (result, error, id) => ([{ type: 'User', id }])
+        }),
+        saveUser: builder.mutation({
+            query: ({ user }) => ({
+                document: gql`
+                            mutation UserUpsert($user: UserInput) {
+                                UserUpsert(user: $user) {
+                                    _id acl
+                                }
+                            }
+                        `,
+                variables: { user }
+            }),
+            invalidatesTags: (result, error, arg) => ([{ type: 'User', id: arg._id }])
+        }),
+        getUsers: builder.query({
+            query: ({ fromPage, pageSize, searchStr = '' }) => {
+                let params = createFullQuery(getUsersSearchParams(searchStr), { fromPage, pageSize, sort: { _id: -1 } });
+                return {
+                    document: gql`
+                        query UserFind($q: String) {
+                            UserFind(query: $q){
+                                _id login nick acl avatar {_id url} createdAt
+                            } 
+                        }                
+                    `,
+                    variables: params
+                }
+            },
+        }),
+        getUsersCount: builder.query({
+            query: ({ searchStr = '' }) => {
+                let params = createFullQuery(getUsersSearchParams(searchStr));
+                return {
+                    document: gql`
+                            query UsersCount($q: String) { UserCount(query: $q) }
+                    `,
+                    variables: params
+                }
+            },
+        }),
+
+    }),
+})
+
+const authSlice = createSlice({
+    name: 'auth',
+    initialState: {},
+    reducers: {
+        logout(state) { 
+            history.push('/');
+            return {}
+        }
+    },
+    extraReducers: builder => {
+        builder.addMatcher(authApi.endpoints.login.matchFulfilled,
+            (state, { payload }) => {
+                const tokenPayload = jwtDecode(payload.login);
+                if (tokenPayload) {
+                    state.token = payload.login;
+                    state.payload = tokenPayload;
+                    state.currentUser = { _id: tokenPayload.sub.id };
+                    history.push('/');
+                }
+            });
+        builder.addMatcher(authApi.endpoints.userFind.matchFulfilled,
+            (state, { payload }) => {
+                let retrievedUser = payload?.UserFindOne;
+                if (retrievedUser?._id === state.currentUser?._id)
+                    state.currentUser = retrievedUser;
+            });
+    }
+})
+
+const actionAboutMe = () =>
+    async (dispatch, getState) => {
+        const auth = getState().auth
+        if (auth.token) {
+            dispatch(authApi.endpoints.userFind.initiate(auth.currentUser._id))
+        }
+    }
+
+const getCurrentUser = state => state.auth?.currentUser ?? {};
+const isCurrentUserAdmin = state =>{
+    let currentUser = getCurrentUser(state);
+    return currentUser ? new UserEntity(currentUser).isAdminRole : false;
+}
+
+const { logout: actionAuthLogout } = authSlice.actions;
+
+export const { useLoginMutation, useUserFindQuery, useSaveUserMutation, useGetUsersQuery, useGetUsersCountQuery, useRegisterMutation } = authApi;
+export { authApi, authSlice, actionAuthLogout, actionAboutMe, getCurrentUser, isCurrentUserAdmin  }
+

+ 116 - 0
src/reducers/cartReducer.js

@@ -0,0 +1,116 @@
+import { createSlice } from "@reduxjs/toolkit"
+import { v4 } from "uuid";
+import { history } from "../App";
+import { findObjectIndexById } from "../utills";
+import { ordersApi } from "./ordersReducer";
+
+const cartSlice = createSlice({ 
+    name: 'cart', //префикс типа наподобие AUTH_
+    initialState: {
+        goods: []
+    },
+    reducers: {
+        restoreCart(state) {
+            let goods = localStorage.cart?.goods ?? [];
+            if (!goods) {
+                goods = [];
+                localStorage.cart = { goods: goods };
+            }
+            setStateData(state, goods, v4());
+            return state;
+        },
+        cleanCart(state) {
+            return cleanCartInt(state);
+        },
+        refreshCart(state) {
+            state.uniqueId = v4();
+            return state;
+        },
+        addGood(state, action) {
+            let { _id, count = 1 } = action.payload.good;
+            let goods = state.goods;
+            let goodIdx = findObjectIndexById(goods, _id);
+            let good;
+            if (goodIdx < 0) {
+                goodIdx = goods.length;
+                good = { _id: _id, count: 0 }
+            }
+            else {
+                good = goods[goodIdx];
+            }
+
+            count = good.count + count;
+            if (count > 0) {
+                good.count = count;
+                state.goods[goodIdx] = good;
+                state.uniqueId = v4()
+            }
+            return state;
+        },
+        deleteGood(state, action) {
+            let { _id } = action.payload.good;
+            let goods = state.goods;
+            let goodIdx = findObjectIndexById(goods, _id);
+            if (goodIdx >= 0) {
+                goods.splice(goodIdx, 1);
+                state.goods = goods;
+                state.uniqueId = v4()
+            }
+            return state;
+        }
+    },
+    extraReducers: builder => {
+        builder.addMatcher(ordersApi.endpoints.addOrder.matchFulfilled,
+            (state, { payload }) => {
+                cleanCartInt(state);
+                let orderId = payload.OrderUpsert._id;
+                history.push(`/order/${orderId}`);
+            });
+    }
+})
+
+const getCartItemsCount = state => {
+    return state.cart?.goods?.reduce((sum, g) => sum + g.count, 0);
+}
+
+function cleanCartInt(state) {
+    localStorage.cart = { goods: [] };
+    setStateData(state, [], v4());
+    return state;
+}
+
+let actionAddGoodToCart = (good, count = 1) =>
+    async (dispatch, state) => {
+        dispatch(cartSlice.actions.addGood({ good: { ...good, count } }))
+    }
+
+let actionDeleteGoodFromCart = good =>
+    async dispatch => {
+        dispatch(cartSlice.actions.deleteGood({ good }))
+    }
+
+let actionRestoreCart = () =>
+    async dispatch => {
+        dispatch(cartSlice.actions.restoreCart({}))
+    }
+
+let actionClearCart = () =>
+    async dispatch => {
+        dispatch(cartSlice.actions.cleanCart({}))
+    }
+
+const setStateData = (state, goods, uniqueId = undefined) => {
+    if (goods !== undefined)
+        state.goods = goods;
+    if (uniqueId !== undefined)
+        state.uniqueId = uniqueId;
+}
+
+
+
+export {
+    cartSlice, 
+    actionAddGoodToCart, actionDeleteGoodFromCart, actionRestoreCart,
+    actionClearCart,
+    getCartItemsCount
+};

+ 147 - 0
src/reducers/categoryReducer.js

@@ -0,0 +1,147 @@
+import { createApi } from '@reduxjs/toolkit/query/react'
+import { graphqlRequestBaseQuery } from "@rtk-query/graphql-request-base-query"
+import { gql } from "graphql-request";
+import { createFullQuery, getFullBackendUrl, repeatQuery } from '../utills';
+
+const getCategorySearchParams = (query, queryExt) => ({ searchStr: query, searchFieldNames: ["name"], queryExt });
+export const prepareHeaders = (headers, { getState }) => {
+    const token = getState().auth.token;
+    if (token) {
+        headers.set("Authorization", `Bearer ${token}`);
+    }
+    return headers;
+}
+
+let placeHolder = '|--|';
+export const categoryApi = createApi({
+    reducerPath: 'category',
+    baseQuery: graphqlRequestBaseQuery({
+        url: getFullBackendUrl('/graphql'),
+        prepareHeaders
+    }),
+    tagTypes: ['Category', 'CategoryCount'],
+    endpoints: (builder) => ({
+        getRootCategories: builder.query({
+            query: (childrenDepth = 0) => {
+                let params = createFullQuery({ queryExt: {parent: null } }, { sort: { name: 1 } });
+                return (
+                    {
+                        document: gql`
+                        query GetCategories($q: String){
+                            CategoryFind(query: $q) {
+                                _id name image { _id url }
+                                ${repeatQuery(childrenDepth, ` subCategories { _id name image { _id url } ${placeHolder} } `, placeHolder)}
+                            }
+                        }
+                    `,
+                        variables: params
+                    }
+                )
+            },
+            providesTags: (result) => {
+                return result
+                    ? [...result.CategoryFind.map(obj => ({ type: 'Category', _id: obj._id })), 'Category']
+                    : ['Category'];
+            },
+            transformResponse: (response) => {
+                return response;
+            },
+            transformErrorResponse: (response, meta) => {
+                return response;
+            },
+        }),
+        getCategories: builder.query({
+            query: ({ withOwner = false, withChildren = false, withParent = false, queryExt = {}, fromPage, pageSize, searchStr = '' }) => {
+                let params = createFullQuery(getCategorySearchParams(searchStr, queryExt), { fromPage, pageSize, sort: { name: 1 } });
+                return {
+                    document: gql`
+                    query GetCategories($q: String){
+                        CategoryFind(query: $q) {
+                            _id name ${withChildren ? 'subCategories { _id name } ' : ''} image { _id url }
+                            ${withParent ? 'parent { _id name } ' : ''}
+                            ${withOwner ? 'owner { _id login nick} ' : ''}
+                            }
+                        }
+                `,
+                    variables: params
+                }
+            },
+            providesTags: (result) => {
+                return result
+                    ? [...result.CategoryFind.map(obj => ({ type: 'Category', _id: obj._id })), 'Category']
+                    : ['Category'];
+            }
+        }),
+        getCategoriesCount: builder.query({
+            query: ({ searchStr = '', queryExt = {} }) => {
+                let params = createFullQuery(getCategorySearchParams(searchStr, queryExt = {}));
+                return {
+                    document: gql`
+                            query CategoriesCount($q: String) { CategoryCount(query: $q) }
+                    `,
+                    variables: params
+                }
+            },
+            providesTags: ['CategoryCount'],
+        }),
+        getCategoryById: builder.query({
+            query: (_id) => ({
+                document: gql`
+                    query GetCategory($q: String) {
+                        CategoryFindOne(query: $q) {
+                            _id name image { _id url }
+                            parent { _id name }
+                            subCategories { _id name }
+                            goods { _id name price description 
+                                images { url }
+                            }
+                        }
+                    }
+                    `,
+                variables: { q: JSON.stringify([{ _id }]) }
+            }),
+            providesTags: (result) => {
+                return result
+                    ? [{ type: 'Category', _id: result.CategoryFindOne._id }, 'Category']
+                    : ['Category'];
+            },
+            transformResponse: (response) => {
+                return response;
+            },
+            transformErrorResponse: (response) => {
+                return response;
+            },
+        }),
+        saveCategory: builder.mutation({
+            query: ({ category }) => (
+                {
+                    document: gql`
+                    mutation SaveCategory($category: CategoryInput ) {
+                        CategoryUpsert(category: $category) {
+                           _id
+                          name
+                           parent { _id name }
+                           }
+                       }
+                        `,
+                    variables: { category: { ...category } }
+                }
+            ),
+            transformResponse: (response) => {
+                return response;
+            },
+            transformErrorResponse: (response) => {
+                return response;
+            },
+            invalidatesTags: (result, error, arg) => {
+                if (!error) {
+                    let catInv = { type: 'Category', _id: arg.category._id };
+                    return [catInv, 'CategoryCount'];
+                }
+            },
+        }),
+    }),
+})
+
+export const { useGetRootCategoriesQuery, useGetCategoryByIdQuery, useGetCategoriesQuery, useGetCategoriesCountQuery, useSaveCategoryMutation } = categoryApi;
+

+ 127 - 0
src/reducers/frontEndReducer.js

@@ -0,0 +1,127 @@
+import { createSlice } from "@reduxjs/toolkit"
+import { categoryApi } from "./categoryReducer";
+import { goodsApi } from "./goodsReducer";
+import { ordersApi } from "./ordersReducer";
+import { authApi } from "./authReducer";
+import { capitalize } from "../utills";
+
+export class frontEndNames {
+    static category = "category";
+    static orders = "orders";
+    static users = "users";
+    static goods = "goods";
+    static entitiesPagingName = name => `${name}Paging`;
+    static currentEntityName = name => `current${capitalize(name)}`;
+    static entitiesCountName = name => `${name}Count`;
+    static searchStrName = name => `${name}SearchStr`;
+}
+
+const frontEndSlice = createSlice({ //promiseReducer
+    name: 'frontend', //префикс типа наподобие AUTH_
+    initialState: {
+        sidebar: {},
+        [frontEndNames.entitiesPagingName(frontEndNames.orders)]: { fromPage: 0, pageSize: 10 },
+        [frontEndNames.entitiesPagingName(frontEndNames.users)]: { fromPage: 0, pageSize: 10 },
+        [frontEndNames.entitiesPagingName(frontEndNames.goods)]: { fromPage: 0, pageSize: 5 },
+        [frontEndNames.entitiesPagingName(frontEndNames.category)]: { fromPage: 0, pageSize: 5 }
+    }, //state={} в параметрах
+    reducers: {
+        setSidebar(state, action) {
+            state.sidebar = { opened: action.payload.open };
+            return state;
+        },
+        setPaging(state, action) {
+            let name = action.payload.name;
+            let { fromPage, pageSize } = action.payload;
+            let paging = state[frontEndNames.entitiesPagingName(name)];
+            fromPage = fromPage ?? paging?.fromPage;
+            pageSize = pageSize ?? paging?.pageSize;
+            state[frontEndNames.entitiesPagingName(name)] = { fromPage, pageSize };
+            return state;
+        },
+        setSearch(state, action) {
+            state[frontEndNames.searchStrName(action.payload.name)] = action.payload.searchStr;
+        },
+        setCurrent(state, action) {
+            state[frontEndNames.currentEntityName(action.payload.name)] = { payload: action.payload.entity };
+            return state;
+        },
+
+    },
+    extraReducers: builder => {
+        builder.addMatcher(goodsApi.endpoints.getGoodsCount.matchFulfilled,
+            (state, { payload }) => {
+                state.goods = { goodsCount: { payload: payload.GoodCount } }
+            });
+        builder.addMatcher(categoryApi.endpoints.getCategoryById.matchFulfilled,
+            (state, { payload }) => {
+                state.goodsPaging.fromPage = 0;
+            });
+        builder.addMatcher(ordersApi.endpoints.getOrdersCount.matchFulfilled,
+            (state, { payload }) => {
+                setEntitiesCount(frontEndNames.orders, state, payload.OrderCount);
+            });
+        builder.addMatcher(authApi.endpoints.getUsersCount.matchFulfilled,
+            (state, { payload }) => {
+                setEntitiesCount(frontEndNames.users, state, payload.UserCount);
+            });
+        builder.addMatcher(categoryApi.endpoints.getCategoriesCount.matchFulfilled,
+            (state, { payload }) => {
+                setEntitiesCount(frontEndNames.category, state, payload.CategoryCount);
+            });
+    }
+})
+
+
+let actionSetPaging = (name, { fromPage, pageSize }) =>
+    async dispatch => {
+        dispatch(frontEndSlice.actions.setPaging({ fromPage, pageSize, name }))
+    }
+
+let actionSetCurrentEntity = (name, entity) =>
+    async dispatch => {
+        dispatch(frontEndSlice.actions.setCurrent({ entity, name }))
+    }
+
+let actionSetSearch = (name, searchStr) =>
+    async dispatch => {
+        dispatch(frontEndSlice.actions.setSearch({ searchStr, name }));
+    }
+
+const getEntitiesPaging = (name, state) => {
+    let paging = state.frontend[frontEndNames.entitiesPagingName(name)];
+    return { fromPage: paging.fromPage, pageSize: paging.pageSize };
+}
+
+const getEntitiesSearchStr = (name, state) => {
+    return { searchStr: state.frontend[frontEndNames.searchStrName(name)] };
+}
+
+const getCurrentEntity = (name, state) => {
+    let result = state.frontend[frontEndNames.currentEntityName(name)]?.payload;
+    return result;
+}
+
+const setEntitiesCount = (name, state, count) => {
+    state[name] = { [frontEndNames.entitiesCountName(name)]: { payload: count } }
+    return state;
+}
+const getEntitiesCount = (name, state) => {
+    return state.frontend[name][frontEndNames.entitiesCountName(name)]?.payload ?? 0;
+}
+
+const getEntitiesListShowParams = (name, state) => {
+    return { ...getEntitiesPaging(name, state), ...getEntitiesSearchStr(name, state) };
+}
+
+const actionSetSidebar = open =>
+    async dispatch => {
+        dispatch(frontEndSlice.actions.setSidebar({ open }))
+    }
+
+const getIsSideBarOpen = state => {
+    return state.frontend?.sidebar.opened === true;
+}
+
+export { frontEndSlice, actionSetSidebar };
+export { getIsSideBarOpen, actionSetPaging, actionSetSearch, getEntitiesCount, getCurrentEntity, actionSetCurrentEntity, getEntitiesSearchStr, getEntitiesPaging, getEntitiesListShowParams }

+ 132 - 0
src/reducers/goodsReducer.js

@@ -0,0 +1,132 @@
+import { createApi } from '@reduxjs/toolkit/query/react'
+import { graphqlRequestBaseQuery } from "@rtk-query/graphql-request-base-query"
+import { gql } from "graphql-request";
+import { createFullQuery, getFullBackendUrl } from '../utills';
+
+export const prepareHeaders = (headers, { getState }) => {
+    const token = getState().auth.token;
+    if (token) {
+        headers.set("Authorization", `Bearer ${token}`);
+    }
+    return headers;
+}
+const getGoodsSearchParams = (searchStr, queryExt) => (
+    {
+        searchStr: searchStr,
+        searchFieldNames: ["name", "description"],
+        queryExt
+    });
+
+export const goodsApi = createApi({
+    reducerPath: 'goods',
+    baseQuery: graphqlRequestBaseQuery({
+        url: getFullBackendUrl('/graphql'),
+        prepareHeaders
+    }),
+    tagTypes: ['Good', 'GoodCount'],
+    endpoints: (builder) => ({
+        getGoods: builder.query({
+            query: ({ fromPage, pageSize, searchStr = '', queryExt = {} }) => {
+                let params = createFullQuery(
+                    getGoodsSearchParams(searchStr, queryExt),
+                    { fromPage, pageSize });
+                return {
+                    document: gql`
+                        query GoodFind($q: String) {
+                            GoodFind(query: $q) {
+                                _id name  price description
+                                images { _id url }
+                            }
+                        }
+                `,
+                    variables: params
+                }
+            },
+            providesTags: (result) => {
+                return result
+                    ? [...result.GoodFind.map(obj => ({ type: 'Good', _id: obj._id })), 'Good']
+                    : ['Good'];
+            }
+        }),
+        getGoodsCount: builder.query({
+            query: ({ searchStr = '', queryExt = {} }) => {
+                let params = createFullQuery(
+                    getGoodsSearchParams(searchStr, queryExt));
+                return {
+                    document: gql`
+                        query GoodsCount($q: String) { GoodCount(query: $q) }
+                    `,
+                    variables: params
+                }
+            },
+            providesTags: ['GoodCount'],
+        }),
+        getGoodById: builder.query({
+            query: (_id) => {
+                let params = createFullQuery({ searchStr: _id, searchFieldNames: ["_id"] });
+                return {
+                    document: gql`
+                        query GoodFindOne($q: String) {
+                            GoodFindOne(query: $q) {
+                                _id name  price description categories { _id name }
+                                images { _id url }
+                            }
+                        }
+                    `,
+                    variables: params
+                }
+            },
+            providesTags: (result) => {
+                return result
+                    ? [{ type: 'Good', _id: result.GoodFindOne?._id }, 'Good']
+                    : ['Good'];
+            }
+        }),
+        getGoodsById: builder.query({
+            query: ({ goods }) => {
+                let params = createFullQuery({queryExt: { _id: { "$in": goods.map(g => g._id) } } })
+                return {
+                    document: gql`
+                        query GoodFind($q: String) {
+                            GoodFind(query: $q) {
+                                _id name  price description
+                                images { url }
+                            }
+                        }
+                    `,
+                    variables: params
+                }
+            },
+            providesTags: (result) => {
+                return result
+                    ? [...result.GoodFind.map(obj => ({ type: 'Good', _id: obj._id })), 'Good']
+                    : ['Good'];
+            }
+        }),
+        saveGood: builder.mutation({
+            query: ({ good }) => {
+                return (
+                    {
+                        document: gql`
+                            mutation GoodUpsert($good: GoodInput) {
+                                GoodUpsert(good: $good) {
+                                    _id
+                                }
+                            }
+                        `,
+                        variables: { good: { ...good, images: good?.images.map(img => ({ _id: img._id })) ?? [] } }
+                    }
+                )
+            },
+            invalidatesTags: (result, error, arg) => {
+                if (!error) {
+                    let goodInv = { type: 'Good', _id: arg.good._id };
+                    return [goodInv, 'GoodCount'];
+                }
+            },
+        }),
+    }),
+})
+
+export const { useGetGoodsQuery, useGetGoodsCountQuery, useGetGoodByIdQuery, useGetGoodsByIdQuery, useSaveGoodMutation } = goodsApi;
+

+ 9 - 0
src/reducers/index.js

@@ -0,0 +1,9 @@
+export { authApi, authSlice, authApi as loginApi, useUserFindQuery, actionAuthLogout, useGetUsersQuery, useGetUsersCountQuery, useSaveUserMutation, getCurrentUser, isCurrentUserAdmin } from './authReducer';
+export { cartSlice, actionAddGoodToCart, actionDeleteGoodFromCart, actionRestoreCart, actionClearCart, getCartItemsCount } from "./cartReducer";
+export { frontEndSlice, frontEndNames, actionSetSidebar, actionSetPaging, actionSetSearch, getEntitiesCount, getCurrentEntity, actionSetCurrentEntity, getEntitiesListShowParams, getEntitiesSearchStr, getEntitiesPaging, getIsSideBarOpen } from "./frontEndReducer";
+export { useGetRootCategoriesQuery, useGetCategoryByIdQuery, useGetCategoriesQuery, useGetCategoriesCountQuery, useSaveCategoryMutation } from './categoryReducer';
+export { ordersApi, useGetOrderByIdQuery, useGetOrdersCountQuery, useGetOrdersQuery, useAddOrderMutation } from './ordersReducer';
+export { goodsApi, useGetGoodByIdQuery, useGetGoodsCountQuery, useGetGoodsQuery, useGetGoodsByIdQuery, useSaveGoodMutation } from './goodsReducer';
+export let DefaultSubCategoriesTreeDepth = 3;
+
+

+ 141 - 0
src/reducers/ordersReducer.js

@@ -0,0 +1,141 @@
+import { createApi } from '@reduxjs/toolkit/query/react'
+import { graphqlRequestBaseQuery } from "@rtk-query/graphql-request-base-query"
+import { gql } from "graphql-request";
+import { createFullQuery, getFullBackendUrl } from '../utills';
+
+const getOrderSearchParams = (query, queryExt) => ({ searchStr: query, searchFieldNames: ["_id"], queryExt });
+const prepareHeaders = (headers, { getState }) => {
+    const token = getState().auth.token;
+    if (token) {
+        headers.set("Authorization", `Bearer ${token}`);
+    }
+    return headers;
+}
+
+const ordersApi = createApi({
+    reducerPath: 'orders',
+    baseQuery: graphqlRequestBaseQuery({
+        url: getFullBackendUrl('/graphql'),
+        prepareHeaders
+    }),
+    tagTypes: ['Order', 'OrderCount'],
+    endpoints: (builder) => ({
+        getOrders: builder.query({
+            query: ({ owner, fromPage, pageSize, searchStr = '' }) => {
+                let queryOrders = GetOwnerQuery(owner);
+                let params = createFullQuery(getOrderSearchParams(searchStr, queryOrders), { fromPage, pageSize, sort: { _id: -1 } });
+                return {
+                    document: gql`
+                            query OrderFind($q: String) {
+                                OrderFind(query: $q) {
+                                    _id total createdAt 
+                                    owner {
+                                        _id nick login
+                                    }
+                                    orderGoods {
+                                        _id price count total createdAt
+                                        good {
+                                            name 
+                                            images { url }
+                                        }
+                                    }
+                                }
+                            }
+                `,
+                    variables: params
+                }
+            },
+            providesTags: (result) => {
+                return result
+                    ? [...result.OrderFind.map(obj => ({ type: 'Order', _id: obj._id })), 'Order']
+                    : ['Order'];
+            },
+            transformResponse: (response) => {
+                return response;
+            },
+            transformErrorResponse: (response, meta) => {
+                return {...response, ...meta.response?.data} ;
+            },
+        }),
+        getOrdersCount: builder.query({
+            query: ({ owner, searchStr = '' }) => {
+                let queryOrders = GetOwnerQuery(owner);
+                let params = createFullQuery(getOrderSearchParams(searchStr, queryOrders));
+                return {
+                    document: gql`
+                            query OrdersCount($q: String) { OrderCount(query: $q) }
+                    `,
+                    variables: params
+                }
+            },
+            providesTags: ['OrderCount'],
+        }),
+        getOrderById: builder.query({
+            query: ({ owner, _id }) => {
+                let queryOrders = GetOwnerQuery(owner);
+                let params = createFullQuery({ queryExt: { ...queryOrders, _id } });
+                return {
+                    document: gql`
+                            query OrderFindOne($q: String) {
+                                OrderFindOne(query: $q) {
+                                    _id total createdAt 
+                                    owner {
+                                        _id nick login
+                                    }
+                                    orderGoods {
+                                        _id price count total createdAt
+                                        good {
+                                            _id
+                                            name 
+                                            images { url }
+                                        }
+                                    }
+                                }
+                            }
+                    `,
+                    variables: params
+                }
+            },
+            providesTags: (result) => {
+                return result
+                    ? [{ type: 'Order', _id: result.OrderFindOne._id }, 'Order']
+                    : ['Order'];
+            },
+            transformResponse: (response) => {
+                return response;
+            },
+            transformErrorResponse: (response, meta) => {
+                return {...response, ...meta.response?.data} ;
+            },
+        }),
+        addOrder: builder.mutation({
+            query: ({ order, id = null }) => (
+                {
+                    document: gql`
+                        mutation OrderUpsert($order: OrderInput) {
+                            OrderUpsert(order: $order) {
+                                _id
+                            } 
+                        }
+                        `,
+                    variables: { order: { "_id": id, "orderGoods": order } }
+                }
+            ),
+            invalidatesTags: (result, error, arg) => {
+                if (!error) {
+                    let orderInv = { type: 'Order', _id: arg.order._id };
+                    return [orderInv, 'OrderCount'];
+                }
+            },
+        }),
+    }),
+});
+
+
+export const { useGetOrdersQuery, useGetOrdersCountQuery, useGetOrderByIdQuery, useAddOrderMutation } = ordersApi;
+export { ordersApi };
+
+function GetOwnerQuery(owner) {
+    return owner?._id && !owner.isAdminRole ? { ___owner: { $in: [owner._id] } } : {};
+}
+

+ 13 - 0
src/reportWebVitals.js

@@ -0,0 +1,13 @@
+const reportWebVitals = onPerfEntry => {
+  if (onPerfEntry && onPerfEntry instanceof Function) {
+    import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
+      getCLS(onPerfEntry);
+      getFID(onPerfEntry);
+      getFCP(onPerfEntry);
+      getLCP(onPerfEntry);
+      getTTFB(onPerfEntry);
+    });
+  }
+};
+
+export default reportWebVitals;

+ 5 - 0
src/setupTests.js

@@ -0,0 +1,5 @@
+// jest-dom adds custom jest matchers for asserting on DOM nodes.
+// allows you to do things like:
+// expect(element).toHaveTextContent(/react/i)
+// learn more: https://github.com/testing-library/jest-dom
+import '@testing-library/jest-dom';

+ 54 - 0
src/store.js

@@ -0,0 +1,54 @@
+import storage from "redux-persist/lib/storage";
+import { combineReducers, configureStore } from '@reduxjs/toolkit';
+import { frontEndSlice, goodsApi } from './reducers';
+import { categoryApi } from './reducers/categoryReducer';
+import { ordersApi } from './reducers/ordersReducer';
+import { authApi, authSlice, cartSlice } from './reducers';
+import {
+  persistReducer, persistStore, FLUSH,
+  REHYDRATE,
+  PAUSE,
+  PERSIST,
+  PURGE,
+  REGISTER
+} from 'redux-persist';
+import thunk from 'redux-thunk';
+
+const persistConfig = {
+  key: 'root',
+  storage,
+  blacklist: [
+    //authSlice.name,
+    authApi.reducerPath,
+    //cartSlice.name,
+    categoryApi.reducerPath,
+    goodsApi.reducerPath,
+    ordersApi.reducerPath,
+    frontEndSlice.name,
+  ]
+};
+const combineReducer = combineReducers({
+  [authSlice.name]: authSlice.reducer,
+  [authApi.reducerPath]: authApi.reducer,
+  [categoryApi.reducerPath]: categoryApi.reducer,
+  [goodsApi.reducerPath]: goodsApi.reducer,
+  [ordersApi.reducerPath]: ordersApi.reducer,
+  [frontEndSlice.name]: frontEndSlice.reducer,
+  [cartSlice.name]: cartSlice.reducer,
+});
+
+const rootReducer = persistReducer(persistConfig, combineReducer);
+
+export const store = configureStore({
+  middleware: (getDefaultMiddleware) => [
+    thunk,
+    ...getDefaultMiddleware({ serializableCheck: { ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER] } }),
+    categoryApi.middleware,
+    goodsApi.middleware,
+    ordersApi.middleware,
+    authApi.middleware,
+  ],
+  reducer: rootReducer
+});
+store.subscribe(() => console.log(store.getState()));
+export const persistedStore = persistStore(store);

+ 41 - 0
src/utills/gqlUtils.js

@@ -0,0 +1,41 @@
+const createQuery = (searchStr, searchFieldNames) => {
+    let result = [];
+    if (searchStr) {
+        for (let searchFieldName of searchFieldNames) {
+            result.push({ [searchFieldName]: searchFieldName === '_id' ? searchStr : `/${searchStr}/` });
+        }
+    }
+    return result.length === 0 ? {} :  { $or: result };
+}
+
+const createQueryExt = (searchQuery = {}, queryExt = {}) => {
+    if (!queryExt)
+        return searchQuery;
+    return { $and: [searchQuery, queryExt] };
+}
+
+const createQueryPaging = (fromPage, pageSize, sort) => {
+    let result = {};
+    if (fromPage !== undefined && pageSize !== undefined) {
+        result["skip"] = [fromPage * pageSize];
+        result["limit"] = [pageSize];
+    }
+    if (sort)
+        result["sort"] = [sort];
+    return result;
+}
+
+export const createFullQuery = ({ searchStr, searchFieldNames, queryExt = {} }, { fromPage, pageSize, sort } = {}) => {
+    return { q: JSON.stringify([createQueryExt(createQuery(searchStr, searchFieldNames), queryExt), createQueryPaging(fromPage, pageSize, sort)]) };
+}
+
+export const repeatQuery = (depth, content, placeHolder, query = undefined) => {
+    query ||= placeHolder;
+    query = ` ${query} `
+    for (let i = 0; i < depth; i++) {
+        query = query.replace("|--|", content);
+    }
+    query = query.replace("|--|", "");
+    return query;
+}
+

+ 2 - 0
src/utills/index.js

@@ -0,0 +1,2 @@
+export {getFullImageUrl, findObjectIndexById, capitalize, fixBackendDataError, jwtDecode, getFullBackendUrl} from './utils';
+export {createFullQuery, repeatQuery} from './gqlUtils';

+ 59 - 0
src/utills/utils.js

@@ -0,0 +1,59 @@
+const getFullImageUrl = (image) =>
+    getFullBackendUrl(`/${image?.url}`);
+
+const getFullBackendUrl = (path) =>
+    `http://127.0.0.1:3030${path}`;
+    //`http://shop-roles.node.ed.asmer.org.ua${path}`;
+
+
+
+const findObjectIndexById = (objs, goodId) => {
+    return +(objs.findIndex(g => g._id === goodId))
+}
+
+function saveImage(image) {
+    let formData = new FormData();
+    formData.append('photo', image.data);
+    let token = '';
+    if (localStorage["persist:auth"])
+        token = JSON.parse(JSON.parse(localStorage["persist:auth"]).token);
+    else
+        token = JSON.parse(JSON.parse(localStorage["persist:root"]).auth).token;
+    let res = fetch(getFullBackendUrl('/upload'), {
+        method: "POST",
+        headers: token ? { Authorization: 'Bearer ' + token } : {},
+        body: formData
+    }).then(res => res.json());
+    return res;
+}
+
+const capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1);
+
+const fixBackendDataError = (result, propName) => {
+    if (result.error && result.error[propName])
+        return result.error[propName];
+    else if (result.data && result.data[propName])
+        return result.data[propName];
+    return undefined;
+}
+
+function jwtDecode(token) {                         // расщифровки токена авторизации
+    if (!token || typeof token != "string")
+        return undefined;
+    let tokenArr = token.split(".");
+    if (tokenArr.length !== 3)
+        return undefined;
+    try {
+        let tokenJsonStr = atob(tokenArr[1]);
+        let tokenJson = JSON.parse(tokenJsonStr);
+        return tokenJson;
+    }
+    catch (error) {
+        return undefined;
+    }
+}
+
+
+export { getFullImageUrl, findObjectIndexById, saveImage, capitalize, fixBackendDataError, jwtDecode, getFullBackendUrl };
+
+