Initial commit
5
front/Dockerfile
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
FROM nginx:1.16.0-alpine
|
||||||
|
COPY default.conf /etc/nginx/conf.d
|
||||||
|
COPY build /usr/share/nginx/html
|
||||||
|
EXPOSE 80
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
70
front/README.md
Normal file
@@ -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)
|
||||||
26
front/default.conf
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
|
||||||
|
index index.html;
|
||||||
|
location / {
|
||||||
|
try_files $uri /index.html;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
location /api {
|
||||||
|
proxy_pass http://192.168.1.101:8080;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Настройка кэширования статических ресурсов
|
||||||
|
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
|
||||||
|
expires 30d;
|
||||||
|
add_header Cache-Control "public, no-transform";
|
||||||
|
}
|
||||||
|
|
||||||
|
# Включение сжатия Gzip
|
||||||
|
gzip on;
|
||||||
|
gzip_types text/plain application/javascript application/json application/xml;
|
||||||
|
}
|
||||||
19344
front/package-lock.json
generated
Normal file
66
front/package.json
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
{
|
||||||
|
"name": "front",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@emotion/react": "^11.13.0",
|
||||||
|
"@emotion/styled": "^11.13.0",
|
||||||
|
"@fontsource/inter": "5.0.18",
|
||||||
|
"@fontsource/plus-jakarta-sans": "5.0.20",
|
||||||
|
"@fontsource/roboto": "^5.0.14",
|
||||||
|
"@fontsource/roboto-mono": "5.0.18",
|
||||||
|
"@hookform/resolvers": "3.6.0",
|
||||||
|
"@mui/icons-material": "^5.16.6",
|
||||||
|
"@mui/material": "^5.16.6",
|
||||||
|
"@mui/utils": "^5.16.6",
|
||||||
|
"@mui/x-date-pickers": "7.7.1",
|
||||||
|
"@phosphor-icons/react": "2.1.6",
|
||||||
|
"@reduxjs/toolkit": "^2.5.0",
|
||||||
|
"@testing-library/jest-dom": "^5.17.0",
|
||||||
|
"@testing-library/react": "^13.4.0",
|
||||||
|
"@testing-library/user-event": "^13.5.0",
|
||||||
|
"@types/react": "^18.3.3",
|
||||||
|
"apexcharts": "3.49.2",
|
||||||
|
"axios": "^1.7.3",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"dayjs": "1.11.11",
|
||||||
|
"moment": "^2.30.1",
|
||||||
|
"notistack": "^3.0.1",
|
||||||
|
"prop-types": "^15.8.1",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-apexcharts": "1.4.1",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
|
"react-hook-form": "7.52.0",
|
||||||
|
"react-jwt": "^1.2.2",
|
||||||
|
"react-redux": "^9.2.0",
|
||||||
|
"react-router": "^7.1.1",
|
||||||
|
"react-router-dom": "^6.25.1",
|
||||||
|
"react-scripts": "5.0.1",
|
||||||
|
"web-vitals": "^2.1.4",
|
||||||
|
"zod": "3.23.8"
|
||||||
|
},
|
||||||
|
"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
front/public/assets/avatar.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
front/public/assets/cocktails/herosim.jpg
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
front/public/assets/error-404.png
Normal file
|
After Width: | Height: | Size: 166 KiB |
BIN
front/public/assets/ingredients/Aperol.png
Normal file
|
After Width: | Height: | Size: 166 KiB |
23
front/public/assets/logo--dark.svg
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||||
|
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg fill="#212636" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
viewBox="0 0 512 512" xml:space="preserve">
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<rect x="169.69" y="496.792" width="231.799" height="15.208"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<path d="M293.189,297.798L471.835,95.29H245.669C241.764,42.091,197.236,0,143.058,0C86.323,0,40.165,46.158,40.165,102.894
|
||||||
|
c0,56.735,46.157,102.891,102.893,102.891c15.495,0,30.831-3.527,44.722-10.236l90.201,102.25v168.576h-77.876v15.208h170.958
|
||||||
|
v-15.208h-77.875V297.798z M230.415,95.288h-69l48.752-48.751C221.462,59.967,228.82,76.812,230.415,95.288z M150.662,15.552
|
||||||
|
c18.474,1.595,35.331,8.928,48.76,20.222l-48.76,48.76V15.552z M135.454,15.542v68.932L86.701,35.775
|
||||||
|
C100.131,24.481,116.981,17.135,135.454,15.542z M75.947,46.528l48.812,48.76H55.708C57.302,76.811,64.65,59.958,75.947,46.528z
|
||||||
|
M55.706,110.496h57.044l5.601,6.35l-42.408,42.408C64.647,145.824,57.3,128.97,55.706,110.496z M135.454,190.244
|
||||||
|
c-18.475-1.594-35.328-8.94-48.758-20.237l41.735-41.735l7.023,7.961V190.244z M150.662,190.224v-36.751l26.603,30.156
|
||||||
|
C168.817,187.207,159.823,189.424,150.662,190.224z M201.485,188.096l-21.039-23.848h132.009V149.04H167.031l-33.999-38.541
|
||||||
|
h112.92h192.188l-34,38.54h-64.81v15.208h51.395L285.586,283.43L201.485,188.096z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
23
front/public/assets/logo-emblem--dark.svg
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||||
|
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg fill="#212636" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
viewBox="0 0 512 512" xml:space="preserve">
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<rect x="169.69" y="496.792" width="231.799" height="15.208"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<path d="M293.189,297.798L471.835,95.29H245.669C241.764,42.091,197.236,0,143.058,0C86.323,0,40.165,46.158,40.165,102.894
|
||||||
|
c0,56.735,46.157,102.891,102.893,102.891c15.495,0,30.831-3.527,44.722-10.236l90.201,102.25v168.576h-77.876v15.208h170.958
|
||||||
|
v-15.208h-77.875V297.798z M230.415,95.288h-69l48.752-48.751C221.462,59.967,228.82,76.812,230.415,95.288z M150.662,15.552
|
||||||
|
c18.474,1.595,35.331,8.928,48.76,20.222l-48.76,48.76V15.552z M135.454,15.542v68.932L86.701,35.775
|
||||||
|
C100.131,24.481,116.981,17.135,135.454,15.542z M75.947,46.528l48.812,48.76H55.708C57.302,76.811,64.65,59.958,75.947,46.528z
|
||||||
|
M55.706,110.496h57.044l5.601,6.35l-42.408,42.408C64.647,145.824,57.3,128.97,55.706,110.496z M135.454,190.244
|
||||||
|
c-18.475-1.594-35.328-8.94-48.758-20.237l41.735-41.735l7.023,7.961V190.244z M150.662,190.224v-36.751l26.603,30.156
|
||||||
|
C168.817,187.207,159.823,189.424,150.662,190.224z M201.485,188.096l-21.039-23.848h132.009V149.04H167.031l-33.999-38.541
|
||||||
|
h112.92h192.188l-34,38.54h-64.81v15.208h51.395L285.586,283.43L201.485,188.096z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
23
front/public/assets/logo-emblem.svg
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||||
|
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg fill="#ffffff" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
viewBox="0 0 512 512" xml:space="preserve">
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<rect x="169.69" y="496.792" width="231.799" height="15.208"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<path d="M293.189,297.798L471.835,95.29H245.669C241.764,42.091,197.236,0,143.058,0C86.323,0,40.165,46.158,40.165,102.894
|
||||||
|
c0,56.735,46.157,102.891,102.893,102.891c15.495,0,30.831-3.527,44.722-10.236l90.201,102.25v168.576h-77.876v15.208h170.958
|
||||||
|
v-15.208h-77.875V297.798z M230.415,95.288h-69l48.752-48.751C221.462,59.967,228.82,76.812,230.415,95.288z M150.662,15.552
|
||||||
|
c18.474,1.595,35.331,8.928,48.76,20.222l-48.76,48.76V15.552z M135.454,15.542v68.932L86.701,35.775
|
||||||
|
C100.131,24.481,116.981,17.135,135.454,15.542z M75.947,46.528l48.812,48.76H55.708C57.302,76.811,64.65,59.958,75.947,46.528z
|
||||||
|
M55.706,110.496h57.044l5.601,6.35l-42.408,42.408C64.647,145.824,57.3,128.97,55.706,110.496z M135.454,190.244
|
||||||
|
c-18.475-1.594-35.328-8.94-48.758-20.237l41.735-41.735l7.023,7.961V190.244z M150.662,190.224v-36.751l26.603,30.156
|
||||||
|
C168.817,187.207,159.823,189.424,150.662,190.224z M201.485,188.096l-21.039-23.848h132.009V149.04H167.031l-33.999-38.541
|
||||||
|
h112.92h192.188l-34,38.54h-64.81v15.208h51.395L285.586,283.43L201.485,188.096z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
23
front/public/assets/logo.svg
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||||
|
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg fill="#ffffff" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
viewBox="0 0 512 512" xml:space="preserve">
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<rect x="169.69" y="496.792" width="231.799" height="15.208"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<path d="M293.189,297.798L471.835,95.29H245.669C241.764,42.091,197.236,0,143.058,0C86.323,0,40.165,46.158,40.165,102.894
|
||||||
|
c0,56.735,46.157,102.891,102.893,102.891c15.495,0,30.831-3.527,44.722-10.236l90.201,102.25v168.576h-77.876v15.208h170.958
|
||||||
|
v-15.208h-77.875V297.798z M230.415,95.288h-69l48.752-48.751C221.462,59.967,228.82,76.812,230.415,95.288z M150.662,15.552
|
||||||
|
c18.474,1.595,35.331,8.928,48.76,20.222l-48.76,48.76V15.552z M135.454,15.542v68.932L86.701,35.775
|
||||||
|
C100.131,24.481,116.981,17.135,135.454,15.542z M75.947,46.528l48.812,48.76H55.708C57.302,76.811,64.65,59.958,75.947,46.528z
|
||||||
|
M55.706,110.496h57.044l5.601,6.35l-42.408,42.408C64.647,145.824,57.3,128.97,55.706,110.496z M135.454,190.244
|
||||||
|
c-18.475-1.594-35.328-8.94-48.758-20.237l41.735-41.735l7.023,7.961V190.244z M150.662,190.224v-36.751l26.603,30.156
|
||||||
|
C168.817,187.207,159.823,189.424,150.662,190.224z M201.485,188.096l-21.039-23.848h132.009V149.04H167.031l-33.999-38.541
|
||||||
|
h112.92h192.188l-34,38.54h-64.81v15.208h51.395L285.586,283.43L201.485,188.096z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
BIN
front/public/assets/qr.png
Normal file
|
After Width: | Height: | Size: 156 KiB |
BIN
front/public/background.webp
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
front/public/favicon.ico
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
BIN
front/public/img/background.png
Normal file
|
After Width: | Height: | Size: 385 KiB |
43
front/public/index.html
Normal file
@@ -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="Набор коктейлей для вашего бара"
|
||||||
|
/>
|
||||||
|
<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>My Little Bar</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
front/public/logo192.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
front/public/logo512.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
15
front/public/manifest.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"short_name": "MyBar",
|
||||||
|
"name": "My Little Bar",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "favicon.ico",
|
||||||
|
"sizes": "64x64 32x32 24x24 16x16",
|
||||||
|
"type": "image/x-icon"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"start_url": ".",
|
||||||
|
"display": "standalone",
|
||||||
|
"theme_color": "#000000",
|
||||||
|
"background_color": "#ffffff"
|
||||||
|
}
|
||||||
3
front/public/robots.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# https://www.robotstxt.org/robotstxt.html
|
||||||
|
User-agent: *
|
||||||
|
Disallow:
|
||||||
7
front/src/Config.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import {getSiteURL} from "./lib/getSiteUrl";
|
||||||
|
import {LogLevel} from "./lib/Logger";
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
site: {name: 'Bar', description: '', themeColor: '#090a0b', url: getSiteURL()},
|
||||||
|
logLevel: (process.env.NEXT_PUBLIC_LOG_LEVEL) ?? LogLevel.ALL,
|
||||||
|
};
|
||||||
5
front/src/Dockerfile
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
FROM nginx:alpine as nginx
|
||||||
|
WORKDIR /app
|
||||||
|
COPY ../nginx.conf /etc/nginx/nginx.conf
|
||||||
|
EXPOSE 80
|
||||||
|
CMD "nginx" "-g" "daemon off;"
|
||||||
51
front/src/app/App.js
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import {CssBaseline, GlobalStyles} from "@mui/material";
|
||||||
|
import {LocalizationProvider} from "../components/core/LocalizationProvider";
|
||||||
|
import {AuthProvider} from "../context/AuthContext";
|
||||||
|
import {createTTheme} from "../styles/theme/create-theme";
|
||||||
|
import {Experimental_CssVarsProvider as CssVarsProvider} from '@mui/material/styles';
|
||||||
|
import {BrowserRouter as Router} from "react-router-dom";
|
||||||
|
import {NavigationRoutes} from "./NavigationRoutes";
|
||||||
|
import {SnackbarProvider} from 'notistack';
|
||||||
|
import {UserProvider} from "../context/UserContext";
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const theme = createTTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
// Провайдер времени
|
||||||
|
<LocalizationProvider>
|
||||||
|
{/*Провайдер уведомлений*/}
|
||||||
|
<SnackbarProvider maxSnack={6} anchorOrigin={{vertical: 'bottom', horizontal: 'right'}}
|
||||||
|
style={{borderRadius: '10px'}}>
|
||||||
|
{/*Провайдер авторизации*/}
|
||||||
|
<AuthProvider>
|
||||||
|
{/*Провайдер пользователя*/}
|
||||||
|
<UserProvider>
|
||||||
|
{/*Провайдер темы*/}
|
||||||
|
<CssVarsProvider theme={theme}>
|
||||||
|
<CssBaseline/>
|
||||||
|
<GlobalStyles
|
||||||
|
styles={{
|
||||||
|
body: {
|
||||||
|
'--MainNav-height': '56px',
|
||||||
|
'--MainNav-zIndex': 1000,
|
||||||
|
'--SideNav-width': '280px',
|
||||||
|
'--SideNav-zIndex': 1200,
|
||||||
|
'--MobileNav-width': '320px',
|
||||||
|
'--MobileNav-zIndex': 1200,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/*Маршрутизация*/}
|
||||||
|
<Router>
|
||||||
|
<NavigationRoutes/>
|
||||||
|
</Router>
|
||||||
|
</CssVarsProvider>
|
||||||
|
</UserProvider>
|
||||||
|
</AuthProvider>
|
||||||
|
</SnackbarProvider>
|
||||||
|
</LocalizationProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
10
front/src/app/HomeRedirect.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import {paths} from "../path";
|
||||||
|
import {Loading} from "../components/core/Loading";
|
||||||
|
|
||||||
|
export function HomeRedirect({auth}) {
|
||||||
|
const redirectPath = auth ? paths.dashboard.overview : paths.auth.signIn;
|
||||||
|
window.location.replace(redirectPath);
|
||||||
|
return (
|
||||||
|
<Loading loading={true}/>
|
||||||
|
)
|
||||||
|
}
|
||||||
151
front/src/app/NavigationRoutes.js
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
import {Route, Routes} from "react-router-dom";
|
||||||
|
import {paths} from "../path";
|
||||||
|
import {useAuth} from "../hooks/useAuth";
|
||||||
|
import NotFoundPage from "./pages/notFound/NotFoundPage";
|
||||||
|
import {UserLayout} from "./layout/UserLayout";
|
||||||
|
import {HomeRedirect} from "./HomeRedirect";
|
||||||
|
import {PublicLayout} from "./layout/PublicLayout";
|
||||||
|
import QueuePage from "./pages/queue/QueuePage";
|
||||||
|
import LoginPage from "./pages/auth/sign-in/loginPage";
|
||||||
|
import {TelegramCode} from "./pages/auth/sign-in/telegram-code";
|
||||||
|
import {IngredientsPage} from "./pages/ingredients/IngredientsPage";
|
||||||
|
import {MenuPage} from "./pages/cocktails/MenuPage";
|
||||||
|
import {AllCocktailsPage} from "./pages/cocktails/AllCocktailsPage";
|
||||||
|
import {EditIngredientPage} from "./pages/ingredients/EditIngredientPage";
|
||||||
|
import {EditCocktailPage} from "./pages/cocktails/EditCocktailPage";
|
||||||
|
import {MyQueuePage} from "./pages/queue/MyQueuePage";
|
||||||
|
import {VisitorPage} from "./pages/VisitorPage";
|
||||||
|
import {CocktailMenuBarPage} from "./pages/cocktails/CocktailMenuBarPage";
|
||||||
|
import {MyBarPage} from "./pages/MyBarPage";
|
||||||
|
import {useEffect, useState} from "react";
|
||||||
|
|
||||||
|
export function NavigationRoutes() {
|
||||||
|
const {auth} = useAuth();
|
||||||
|
const [loadedRoutes, setLoadedRoutes] = useState(undefined);
|
||||||
|
useEffect(() => {
|
||||||
|
setLoadedRoutes(auth ? authPages : guestPages)
|
||||||
|
}, [auth]);
|
||||||
|
if (!loadedRoutes) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Routes>
|
||||||
|
{loadedRoutes.map((page) => {
|
||||||
|
return (
|
||||||
|
<Route
|
||||||
|
key={page.path + page.isPrivate + page.exact}
|
||||||
|
path={page.path}
|
||||||
|
exact={page.exact}
|
||||||
|
element={<ElementProvider isPrivate={page.isPrivate}>
|
||||||
|
{page.children}
|
||||||
|
</ElementProvider>}/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</Routes>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ElementProvider({isPrivate, children}) {
|
||||||
|
if (isPrivate) {
|
||||||
|
return (<UserLayout>{children}</UserLayout>);
|
||||||
|
} else {
|
||||||
|
return (<PublicLayout>{children}</PublicLayout>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const authPages = [
|
||||||
|
{
|
||||||
|
children: (<HomeRedirect auth={true}/>),
|
||||||
|
isPrivate: false,
|
||||||
|
path: paths.home,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: paths.auth.signIn,
|
||||||
|
children: (<LoginPage/>),
|
||||||
|
isPrivate: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: paths.dashboard.overview,
|
||||||
|
isPrivate: true,
|
||||||
|
children: (<MenuPage/>),
|
||||||
|
exact: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: paths.bar.cocktails,
|
||||||
|
isPrivate: true,
|
||||||
|
children: (<AllCocktailsPage/>)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: paths.bar.list,
|
||||||
|
isPrivate: true,
|
||||||
|
children: (<MyBarPage/>)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: paths.orders.my,
|
||||||
|
isPrivate: true,
|
||||||
|
children: (<MyQueuePage/>)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: paths.bar.ingredients,
|
||||||
|
isPrivate: true,
|
||||||
|
children: (<IngredientsPage/>)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: paths.bar.ordersQueue,
|
||||||
|
isPrivate: true,
|
||||||
|
children: (<QueuePage/>),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: paths.visitor.inBar,
|
||||||
|
isPrivate: true,
|
||||||
|
children: (<VisitorPage/>)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: paths.bar.ingredientEdit,
|
||||||
|
isPrivate: true,
|
||||||
|
forAdmin: true,
|
||||||
|
children: (<EditIngredientPage/>)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: paths.bar.menu,
|
||||||
|
isPrivate: true,
|
||||||
|
children: (<CocktailMenuBarPage/>)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: paths.bar.cocktailEdit,
|
||||||
|
isPrivate: true,
|
||||||
|
forAdmin: true,
|
||||||
|
children: (<EditCocktailPage/>)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: paths.notFound,
|
||||||
|
isPrivate: false,
|
||||||
|
children: (<NotFoundPage/>)
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const guestPages = [
|
||||||
|
{
|
||||||
|
path: paths.home,
|
||||||
|
isPrivate: false,
|
||||||
|
children: (<HomeRedirect auth={false}/>),
|
||||||
|
exact: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: paths.auth.tg,
|
||||||
|
isPrivate: false,
|
||||||
|
children: (<TelegramCode/>),
|
||||||
|
exact: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: paths.auth.signIn,
|
||||||
|
isPrivate:
|
||||||
|
false,
|
||||||
|
children: (<LoginPage/>),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: paths.notFound,
|
||||||
|
isPrivate: false,
|
||||||
|
children: (<NotFoundPage/>),
|
||||||
|
},
|
||||||
|
]
|
||||||
58
front/src/app/layout/PublicLayout.js
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Stack from '@mui/material/Stack';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import {DynamicLogo} from "../../components/core/Logo";
|
||||||
|
import {paths} from "../../path";
|
||||||
|
|
||||||
|
export function PublicLayout({ children }) {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: { xs: 'flex', lg: 'grid' },
|
||||||
|
flexDirection: 'column',
|
||||||
|
gridTemplateColumns: '1fr 1fr',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ display: 'flex', flex: '1 1 auto', flexDirection: 'column' }}>
|
||||||
|
<Box sx={{ p: 3 }}>
|
||||||
|
<Box component={'a'} href={paths.home} sx={{ display: 'inline-block', fontSize: 0 }}>
|
||||||
|
<DynamicLogo colorDark="light" colorLight="dark" height={32} width={122} />
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ alignItems: 'center', display: 'flex', flex: '1 1 auto', justifyContent: 'center', p: 3 }}>
|
||||||
|
<Box sx={{ maxWidth: '450px', width: '100%' }}>{children}</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
alignItems: 'center',
|
||||||
|
background: 'radial-gradient(50% 50% at 50% 50%, #122647 0%, #090E23 100%)',
|
||||||
|
color: 'var(--mui-palette-common-white)',
|
||||||
|
display: { xs: 'none', lg: 'flex' },
|
||||||
|
justifyContent: 'center',
|
||||||
|
p: 3,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack spacing={3}>
|
||||||
|
<Stack spacing={1}>
|
||||||
|
<Typography color="inherit" sx={{ fontSize: '24px', lineHeight: '32px', textAlign: 'center' }} variant="h1">
|
||||||
|
<Box component="span" sx={{ color: '#15b79e' }}>
|
||||||
|
Добро пожаловать в бар
|
||||||
|
</Box>
|
||||||
|
</Typography>
|
||||||
|
<Typography align="center" variant="subtitle1">
|
||||||
|
Самый большой выбор честно спизженных коктейлей
|
||||||
|
</Typography>
|
||||||
|
<Box
|
||||||
|
component="img"
|
||||||
|
alt="Under development"
|
||||||
|
src="/assets/qr.png"
|
||||||
|
sx={{ display: 'inline-block', height: 'auto', maxWidth: '100%', width: '400px' }}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
31
front/src/app/layout/UserLayout.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import {SideNav} from "../../components/navigation/SideNav";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import {MainNav} from "../../components/navigation/MainNav";
|
||||||
|
import Container from "@mui/material/Container";
|
||||||
|
|
||||||
|
export function UserLayout({children}) {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
bgcolor: 'var(--mui-palette-background-default)',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
position: 'relative',
|
||||||
|
minHeight: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SideNav/>
|
||||||
|
<Box sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flex: '1 1 auto',
|
||||||
|
flexDirection: 'column',
|
||||||
|
pl: {xl: 'var(--SideNav-width)'}
|
||||||
|
}}>
|
||||||
|
<MainNav/>
|
||||||
|
<Container maxWidth="xl" sx={{py: '16px'}}>
|
||||||
|
{children}
|
||||||
|
</Container>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
62
front/src/app/pages/MyBarPage.js
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import Toolbar from "@mui/material/Toolbar";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import Paper from "@mui/material/Paper";
|
||||||
|
import {FormControl, InputAdornment, InputLabel, OutlinedInput, Tabs} from "@mui/material";
|
||||||
|
import IconButton from "@mui/material/IconButton";
|
||||||
|
import SearchIcon from "@mui/icons-material/Search";
|
||||||
|
import * as React from "react";
|
||||||
|
import {useState} from "react";
|
||||||
|
import Tab from "@mui/material/Tab";
|
||||||
|
import {a11yProps} from "../../components/core/tabProps";
|
||||||
|
import {CustomTabPanel} from "../../components/core/TabPanel";
|
||||||
|
import {BarList} from "../../components/bar/BarList";
|
||||||
|
|
||||||
|
export function MyBarPage() {
|
||||||
|
const [value, setValue] = React.useState(0);
|
||||||
|
const handleChange = (event, newValue) => setValue(newValue);
|
||||||
|
const [findString, setFindString] = useState("");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
{/*Заголовок*/}
|
||||||
|
<Toolbar>
|
||||||
|
<Typography variant="h6" component="div" sx={{flexGrow: 1}}>Мои бары</Typography>
|
||||||
|
</Toolbar>
|
||||||
|
{/*Поиск*/}
|
||||||
|
<Paper elevation={6} sx={{my: 2}}>
|
||||||
|
<FormControl sx={{m: 1, width: 'calc(100% - 20px'}}>
|
||||||
|
<InputLabel htmlFor="outlined-adornment-amount">Поиск</InputLabel>
|
||||||
|
<OutlinedInput
|
||||||
|
onChange={(e) => setFindString(e.target.value)}
|
||||||
|
label="With normal TextField"
|
||||||
|
startAdornment={
|
||||||
|
<InputAdornment position="start">
|
||||||
|
<IconButton edge="end">
|
||||||
|
<SearchIcon/>
|
||||||
|
</IconButton>
|
||||||
|
</InputAdornment>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</Paper>
|
||||||
|
{/*Рабочее поле ингредиентов*/}
|
||||||
|
<Box>
|
||||||
|
<Tabs value={value} onChange={handleChange} aria-label="basic tabs example">
|
||||||
|
<Tab label="Мои бары" {...a11yProps(0)} />
|
||||||
|
<Tab label="Список" {...a11yProps(1)} />
|
||||||
|
</Tabs>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<CustomTabPanel value={value} index={0}>
|
||||||
|
<BarList all={false} find={findString}/>
|
||||||
|
</CustomTabPanel>
|
||||||
|
<CustomTabPanel value={value} index={1}>
|
||||||
|
<BarList all={true} find={findString}/>
|
||||||
|
</CustomTabPanel>
|
||||||
|
</Box>
|
||||||
|
{/*Модальное окно информации об ингредиенте*/}
|
||||||
|
{/*<IngredientInfoModal ingredient={selectedInfo} open={openModal} closeHandler={handleCloseModal}/>*/}
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
77
front/src/app/pages/VisitorPage.js
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import {useEffect, useState} from "react";
|
||||||
|
import {api} from "../../lib/clients/api";
|
||||||
|
import {requests} from "../../requests";
|
||||||
|
import {useAlert} from "../../hooks/useAlert";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import {VisitorItem} from "../../components/visitor/VisitorItem";
|
||||||
|
import Toolbar from "@mui/material/Toolbar";
|
||||||
|
import * as React from "react";
|
||||||
|
import Button from "@mui/material/Button";
|
||||||
|
import {useUser} from "../../hooks/useUser";
|
||||||
|
|
||||||
|
export function VisitorPage() {
|
||||||
|
const {session, checkSession} = useUser();
|
||||||
|
const [visitors, setVisitors] = useState([])
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const {createError} = useAlert();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
api().get(requests.visitors.all)
|
||||||
|
.then((r) => {
|
||||||
|
setVisitors(r.data)
|
||||||
|
})
|
||||||
|
.catch(() => createError("Ошибка получения данных"))
|
||||||
|
// eslint-disable-next-line
|
||||||
|
}, []);
|
||||||
|
useEffect(() => {
|
||||||
|
setOpen(session.isActive);
|
||||||
|
}, [session, checkSession])
|
||||||
|
|
||||||
|
const changeHandler = (visitor) => {
|
||||||
|
const arr = visitors.map((v) => {
|
||||||
|
if(v.id === visitor.id) {
|
||||||
|
return {
|
||||||
|
...visitor,
|
||||||
|
invited: !visitor.invited
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
})
|
||||||
|
api().post(`${requests.visitors.invite}id=${visitor.id}&value=${!visitor.invited}`)
|
||||||
|
.then(() => setVisitors(arr))
|
||||||
|
.catch(() => createError("Ошибка запроса"))
|
||||||
|
|
||||||
|
}
|
||||||
|
const changeShift = () => {
|
||||||
|
api().post(`${requests.bar.session.change}?value=${!open}`)
|
||||||
|
.then(() => {
|
||||||
|
checkSession?.();
|
||||||
|
setOpen(!open)
|
||||||
|
})
|
||||||
|
.catch(() => createError("Ошибка закрытия сессии"))
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
{/*Заголовок*/}
|
||||||
|
<Toolbar>
|
||||||
|
<Typography variant="h6" component="div" sx={{flexGrow: 1}}>Посетители</Typography>
|
||||||
|
</Toolbar>
|
||||||
|
<Box ml={0} mb={2}>
|
||||||
|
{visitors.map((v) => {
|
||||||
|
return (
|
||||||
|
<VisitorItem key={v.id} visitor={v} changeHandler={changeHandler} open={open}/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant='contained'
|
||||||
|
color={open ? 'error' : 'success'}
|
||||||
|
onClick={() => changeShift()}
|
||||||
|
>{`${open ? "Закрыть " : "Открыть "}смену`}</Button>
|
||||||
|
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
11
front/src/app/pages/auth/sign-in/loginPage.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import {GuestGuard} from "../../../../components/auth/guest-guard";
|
||||||
|
import {SignInForm} from "../../../../components/auth/sign-in-form";
|
||||||
|
|
||||||
|
export default function LoginPage() {
|
||||||
|
return (
|
||||||
|
<GuestGuard>
|
||||||
|
<SignInForm/>
|
||||||
|
</GuestGuard>
|
||||||
|
);
|
||||||
|
}
|
||||||
30
front/src/app/pages/auth/sign-in/telegram-code.js
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import {useSearchParams} from "react-router-dom";
|
||||||
|
import {Loading} from "../../../../components/core/Loading";
|
||||||
|
import {api} from "../../../../lib/clients/api";
|
||||||
|
import {requests} from "../../../../requests";
|
||||||
|
import {useAuth} from "../../../../hooks/useAuth";
|
||||||
|
|
||||||
|
export function TelegramCode() {
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const {checkSession} = useAuth();
|
||||||
|
|
||||||
|
let code = searchParams.get("code");
|
||||||
|
const request = {
|
||||||
|
byLogin: false,
|
||||||
|
code: code
|
||||||
|
}
|
||||||
|
api().post(requests.auth.login, request)
|
||||||
|
.then(async (response) => {
|
||||||
|
if (response.data.error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
localStorage.setItem("token", response.data.token);
|
||||||
|
await checkSession?.();
|
||||||
|
window.location.reload();
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Loading loading={true}/>
|
||||||
|
)
|
||||||
|
}
|
||||||
7
front/src/app/pages/cocktails/AllCocktailsPage.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import CocktailsPageContent from "./CocktailsPageContent";
|
||||||
|
|
||||||
|
export function AllCocktailsPage() {
|
||||||
|
return (
|
||||||
|
<CocktailsPageContent all={true}/>
|
||||||
|
)
|
||||||
|
}
|
||||||
127
front/src/app/pages/cocktails/CocktailMenuBarPage.js
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import Toolbar from "@mui/material/Toolbar";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import Paper from "@mui/material/Paper";
|
||||||
|
import {Fab, FormControl, FormControlLabel, InputAdornment, InputLabel, OutlinedInput} from "@mui/material";
|
||||||
|
import IconButton from "@mui/material/IconButton";
|
||||||
|
import SearchIcon from "@mui/icons-material/Search";
|
||||||
|
import Switch from "@mui/material/Switch";
|
||||||
|
import {blue} from "@mui/material/colors";
|
||||||
|
import UpIcon from "@mui/icons-material/KeyboardArrowUp";
|
||||||
|
import {Loading} from "../../../components/core/Loading";
|
||||||
|
import * as React from "react";
|
||||||
|
import {useEffect, useMemo, useState} from "react";
|
||||||
|
import {CocktailsList} from "../../../components/cocktails/CocktailsList";
|
||||||
|
import {requests} from "../../../requests";
|
||||||
|
import {api} from "../../../lib/clients/api";
|
||||||
|
import {useAlert} from "../../../hooks/useAlert";
|
||||||
|
import {CocktailInfoModal} from "../../../components/cocktails/CocktailInfoModal";
|
||||||
|
|
||||||
|
export function CocktailMenuBarPage() {
|
||||||
|
const {createError} = useAlert();
|
||||||
|
const [grouping, setGrouping] = useState(true);
|
||||||
|
const [findString, setFindString] = useState("");
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [cocktails, setCocktails] = useState([]);
|
||||||
|
const [openModal, setOpenModal] = useState(false);
|
||||||
|
const [selected, setSelected] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
api().get(`${requests.cocktails.menu}?all=true`)
|
||||||
|
.then((r) => {
|
||||||
|
setCocktails(r.data);
|
||||||
|
setLoading(false);
|
||||||
|
})
|
||||||
|
.catch(() => createError("Ошибка получения данных"))
|
||||||
|
// eslint-disable-next-line
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
|
const handleOpenModal = (row) => {
|
||||||
|
setSelected(row)
|
||||||
|
setOpenModal(true);
|
||||||
|
}
|
||||||
|
const changeHandler = (row, value) => {
|
||||||
|
const newState = cocktails.map((r) => {
|
||||||
|
if(r.id !== row.id) {
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...r,
|
||||||
|
inMenu: value
|
||||||
|
}
|
||||||
|
});
|
||||||
|
api().post(`${requests.cocktails.menu}?id=${row.id}&value=${value}`)
|
||||||
|
.then(() => {
|
||||||
|
setCocktails(newState);
|
||||||
|
}).catch(() => createError("Ошибка сохранения данных"))
|
||||||
|
}
|
||||||
|
|
||||||
|
const visibleRows = useMemo(() => {
|
||||||
|
if (findString === "") {
|
||||||
|
return cocktails;
|
||||||
|
}
|
||||||
|
let regExp = new RegExp("(.*?)" + findString + "(.*?)", "i");
|
||||||
|
return cocktails
|
||||||
|
.filter((row) => row.name.split(" ").map((n) => n.match(regExp) !== null).includes(true))
|
||||||
|
// eslint-disable-next-line
|
||||||
|
}, [cocktails, findString])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
{/*Заголовок*/}
|
||||||
|
<Toolbar>
|
||||||
|
<Typography variant="h6" component="div" sx={{flexGrow: 1}}>Меню бара</Typography>
|
||||||
|
</Toolbar>
|
||||||
|
{/*Поиск*/}
|
||||||
|
<Paper elevation={6} sx={{my: 2}}>
|
||||||
|
<FormControl sx={{m: 1, width: 'calc(100% - 20px'}}>
|
||||||
|
<InputLabel htmlFor="outlined-adornment-amount">Поиск</InputLabel>
|
||||||
|
<OutlinedInput
|
||||||
|
onChange={(e) => setFindString(e.target.value)}
|
||||||
|
label="With normal TextField"
|
||||||
|
startAdornment={
|
||||||
|
<InputAdornment position="start">
|
||||||
|
<IconButton edge="end">
|
||||||
|
<SearchIcon/>
|
||||||
|
</IconButton>
|
||||||
|
</InputAdornment>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormControlLabel sx={{ml: '2px'}}
|
||||||
|
control={<Switch defaultChecked/>}
|
||||||
|
onClick={() => setGrouping(!grouping)}
|
||||||
|
label="Группировать"
|
||||||
|
labelPlacement="end"/>
|
||||||
|
</Paper>
|
||||||
|
{/*Рабочее поле коктейлей*/}
|
||||||
|
<CocktailsList rows={visibleRows} changeHandler={changeHandler}
|
||||||
|
infoHandler={handleOpenModal} grouping={grouping}/>
|
||||||
|
{/*Иконка возврата наверх*/}
|
||||||
|
<Fab sx={{
|
||||||
|
alpha: '30%',
|
||||||
|
position: 'sticky',
|
||||||
|
bottom: '16px',
|
||||||
|
color: 'common.white',
|
||||||
|
bgcolor: blue[600],
|
||||||
|
'&:hover': {
|
||||||
|
bgcolor: blue[600],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
onClick={() => window.window.scrollTo(0, 0)}
|
||||||
|
aria-label='Expand'
|
||||||
|
color='inherit'>
|
||||||
|
<UpIcon/>
|
||||||
|
</Fab>
|
||||||
|
{/*Загрузчик*/}
|
||||||
|
<Loading loading={loading}/>
|
||||||
|
{/*Модальное окно информации об ингредиенте*/}
|
||||||
|
<CocktailInfoModal open={openModal} row={selected}
|
||||||
|
closeHandler={() => {
|
||||||
|
setSelected(null);
|
||||||
|
setOpenModal(false);
|
||||||
|
}}/>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
333
front/src/app/pages/cocktails/CocktailsPageContent.js
Normal file
@@ -0,0 +1,333 @@
|
|||||||
|
import Grid from "@mui/material/Grid";
|
||||||
|
import {useAlert} from "../../../hooks/useAlert";
|
||||||
|
import * as React from "react";
|
||||||
|
import {useCallback, useEffect, useState} from "react";
|
||||||
|
import {Cocktail} from "../../../components/cocktails/Cocktail";
|
||||||
|
import {Fab, Skeleton} from "@mui/material";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import {requests} from "../../../requests";
|
||||||
|
import {NoResult} from "../../../components/cocktails/NoResult";
|
||||||
|
import {FilterBlock} from "../../../components/cocktails/FilterBlock";
|
||||||
|
import {api} from "../../../lib/clients/api";
|
||||||
|
import {CocktailInfoModal} from "../../../components/cocktails/CocktailInfoModal";
|
||||||
|
import {useUser} from "../../../hooks/useUser";
|
||||||
|
import {blue} from "@mui/material/colors";
|
||||||
|
import UpIcon from "@mui/icons-material/KeyboardArrowUp";
|
||||||
|
import {sortList} from "../../../components/cocktails/sortingList";
|
||||||
|
import {getComparator} from "../../../components/core/getComparator";
|
||||||
|
import Button from "@mui/material/Button";
|
||||||
|
import Paper from "@mui/material/Paper";
|
||||||
|
import CheckMarks from "../../../components/cocktails/CheckMarks";
|
||||||
|
|
||||||
|
const filterList = (rows, filter, allowIngredients) => {
|
||||||
|
let regExp = new RegExp("(.*?)" + filter.search + "(.*?)", "i");
|
||||||
|
const sortingObj = sortList.find((s) => s.name === filter.sorting);
|
||||||
|
const sortingValues = sortingObj.id.split("|");
|
||||||
|
return rows
|
||||||
|
.filter((row) => {
|
||||||
|
const nameReg = row.name.split(" ").map((n) => n.match(regExp) !== null).includes(true);
|
||||||
|
const ingredientReg = row.components
|
||||||
|
.split(", ")
|
||||||
|
.map((r) => r.match(regExp) !== null)
|
||||||
|
.includes(true);
|
||||||
|
return nameReg || ingredientReg;
|
||||||
|
})
|
||||||
|
.filter((row) => filter.onlyFavourite ? row.rating.favourite : true)
|
||||||
|
.filter((row) => filter.glass.length === 0 || filter.glass.includes(row.glass))
|
||||||
|
.filter((row) => filter.category.length === 0 || filter.category.includes(row.category))
|
||||||
|
.filter((row) => filter.alcohol.length === 0 || filter.alcohol.includes(row.alcoholic))
|
||||||
|
.filter((row) => {
|
||||||
|
if (filter.tags.length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (row.tags.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return row.tags.split(",").find((tag) => filter.tags.includes(tag))
|
||||||
|
})
|
||||||
|
.filter((row) => {
|
||||||
|
if (filter.iCount.length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const arr = row.components.split(", ");
|
||||||
|
const count = arr.filter((n) => !allowIngredients.includes(n)).length;
|
||||||
|
const filt = filter.ingredient.length === 0 || arr.filter((n) => filter.ingredient.includes(n)).length > 0;
|
||||||
|
|
||||||
|
return filter.iCount === count && filt;
|
||||||
|
})
|
||||||
|
.filter((row) => {
|
||||||
|
if (filter.inMenu === "") {
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
const filterValue = filter.inMenu === "Есть в меню";
|
||||||
|
return filterValue === row.inMenu;
|
||||||
|
})
|
||||||
|
.sort(getComparator(sortingValues[1], sortingValues[0], "name"))
|
||||||
|
}
|
||||||
|
|
||||||
|
const emptyFilter = {
|
||||||
|
search: "",
|
||||||
|
hidden: true,
|
||||||
|
onlyFavourite: false,
|
||||||
|
glass: [],
|
||||||
|
category: [],
|
||||||
|
alcohol: [],
|
||||||
|
tags: [],
|
||||||
|
iCount: [],
|
||||||
|
ingredient: [],
|
||||||
|
inMenu: "",
|
||||||
|
sorting: "Название по возрастанию"
|
||||||
|
}
|
||||||
|
|
||||||
|
const CocktailsPageContent = ({all}) => {
|
||||||
|
const {user} = useUser();
|
||||||
|
const {createError, createSuccess} = useAlert();
|
||||||
|
const [allowIngredients, setAllowIngredients] = useState([])
|
||||||
|
const [rows, setRows] = useState([]);
|
||||||
|
const [filter, setFilter] = useState(emptyFilter)
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [selectedCocktail, setSelectedCocktail] = useState(null)
|
||||||
|
const [chips, setChips] = useState([])
|
||||||
|
const [page, setPage] = useState(-1);
|
||||||
|
const [load, setLoad] = useState(false);
|
||||||
|
const [isEnd, setIsEnd] = useState(false);
|
||||||
|
const [isNew, setIsNew] = useState(true);
|
||||||
|
|
||||||
|
const loading = useCallback(() => {
|
||||||
|
const size = Math.floor((window.innerWidth) / 350) * 5;
|
||||||
|
if (load || (!isNew && isEnd)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
setLoad(true);
|
||||||
|
const request = {
|
||||||
|
...filter,
|
||||||
|
all: all,
|
||||||
|
sort: sortList.find((s) => s.name === filter.sorting).id,
|
||||||
|
page: page + 1,
|
||||||
|
size: size,
|
||||||
|
iCount: Array.isArray(filter.iCount) ? null : filter.iCount
|
||||||
|
}
|
||||||
|
api().post(requests.cocktails.menu, request)
|
||||||
|
.then((r) => {
|
||||||
|
if (r.data.length === 0) {
|
||||||
|
if(isNew) {
|
||||||
|
setRows([]);
|
||||||
|
}
|
||||||
|
setIsEnd(true);
|
||||||
|
setLoad(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const cocktails = isNew ? r.data : rows.concat(r.data);
|
||||||
|
setRows(cocktails);
|
||||||
|
setIsNew(false);
|
||||||
|
setPage(page + 1);
|
||||||
|
setLoad(false);
|
||||||
|
})
|
||||||
|
.catch((r) => {
|
||||||
|
setLoad(false);
|
||||||
|
createError("Ошибка загрузки данных от сервера Status:" + r.status)
|
||||||
|
})
|
||||||
|
// eslint-disable-next-line
|
||||||
|
}, [load, isEnd, page]);
|
||||||
|
useEffect(() => {
|
||||||
|
const handleScroll = () => {
|
||||||
|
const {scrollTop, scrollHeight, clientHeight} = document.documentElement;
|
||||||
|
if (scrollTop + clientHeight >= scrollHeight - 100) {
|
||||||
|
loading();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.addEventListener('scroll', handleScroll);
|
||||||
|
return () => window.removeEventListener('scroll', handleScroll);
|
||||||
|
}, [loading]);
|
||||||
|
useEffect(() => {
|
||||||
|
api().get(requests.bar.ingredientSimple)
|
||||||
|
.then((r) => {
|
||||||
|
const arr = r.data.filter((i) => i.isHave)
|
||||||
|
.map((i) => i.name)
|
||||||
|
setAllowIngredients(arr)
|
||||||
|
})
|
||||||
|
.catch(() => createError("Ошибка получения ингредиентов"))
|
||||||
|
// eslint-disable-next-line
|
||||||
|
}, [])
|
||||||
|
useEffect(() => {
|
||||||
|
loading();
|
||||||
|
}, [filter])
|
||||||
|
useEffect(() => {
|
||||||
|
if (!all) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const ingredients = new Set();
|
||||||
|
rows.map((c) => c.components)
|
||||||
|
.map((c) => c.split(", "))
|
||||||
|
.map((c) => c.filter((i) => !allowIngredients.includes(i)))
|
||||||
|
.filter((nhc) => nhc.length === 1)
|
||||||
|
.map((fc) => fc[0])
|
||||||
|
.forEach((i) => ingredients.add(i))
|
||||||
|
setChips(Array.from(ingredients).sort(getComparator()));
|
||||||
|
}, [rows, allowIngredients])
|
||||||
|
|
||||||
|
const renderSkeleton = () => {
|
||||||
|
return Array.from({length: 3}, () => null)
|
||||||
|
.map((v, index) => <Skeleton sx={{m: 2}}
|
||||||
|
key={index}
|
||||||
|
variant="rounded"
|
||||||
|
width={350}
|
||||||
|
height={690}/>);
|
||||||
|
}
|
||||||
|
const handleChangeRating = (row, value) => {
|
||||||
|
const newState = rows.map((r) => {
|
||||||
|
if (row.id === r.id) {
|
||||||
|
let newRating = r.rating;
|
||||||
|
newRating.rating = value
|
||||||
|
return {
|
||||||
|
...r,
|
||||||
|
rating: newRating
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
})
|
||||||
|
api().post(`${requests.cocktails.rating}${row.id}&rating=${value}`)
|
||||||
|
.then(() => {
|
||||||
|
setRows(newState);
|
||||||
|
createSuccess("Спасибо за оценку!")
|
||||||
|
}).catch(() => createError("Ошибка сохранения"))
|
||||||
|
|
||||||
|
}
|
||||||
|
const handleFilterChange = (filterName, value) => {
|
||||||
|
const newState = {
|
||||||
|
...filter,
|
||||||
|
[filterName]: value
|
||||||
|
}
|
||||||
|
setFilter(newState)
|
||||||
|
setIsNew(true);
|
||||||
|
setIsEnd(false);
|
||||||
|
setPage(-1);
|
||||||
|
}
|
||||||
|
const handleFavourite = (row) => {
|
||||||
|
const value = !row.rating.favourite;
|
||||||
|
const newState = rows.map((r) => {
|
||||||
|
if (r.id === row.id) {
|
||||||
|
let newRating = r.rating;
|
||||||
|
newRating.favourite = value;
|
||||||
|
return {
|
||||||
|
...r,
|
||||||
|
rating: newRating
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
});
|
||||||
|
let url = `${requests.cocktails.favourite}${row.id}`;
|
||||||
|
let request = value ? api().put(url) : api().delete(url);
|
||||||
|
|
||||||
|
request
|
||||||
|
.then(() => {
|
||||||
|
setRows(newState);
|
||||||
|
createSuccess("Спасибо за оценку!")
|
||||||
|
}).catch(() => createError("Ошибка сохранения"))
|
||||||
|
}
|
||||||
|
const handleFilterClear = () => {
|
||||||
|
setFilter(emptyFilter);
|
||||||
|
}
|
||||||
|
const handleSelectCocktail = (row) => {
|
||||||
|
setSelectedCocktail(row.id)
|
||||||
|
setOpen(true)
|
||||||
|
}
|
||||||
|
const handleCloseCocktailModal = () => {
|
||||||
|
setOpen(false);
|
||||||
|
setSelectedCocktail(null);
|
||||||
|
}
|
||||||
|
const handleEditMenu = (row, value) => {
|
||||||
|
const newState = rows.map((r) => {
|
||||||
|
if (r.id !== row.id) {
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
if (all) {
|
||||||
|
return {
|
||||||
|
...r,
|
||||||
|
inMenu: value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}).filter((r) => r !== null);
|
||||||
|
|
||||||
|
api().post(`${requests.cocktails.menu}?id=${row.id}&value=${value}`)
|
||||||
|
.then(() => setRows(newState))
|
||||||
|
.catch(() => createError("Ошибка сохранения данных"))
|
||||||
|
}
|
||||||
|
const editMenuBlock = (row) => {
|
||||||
|
if (user.role === "USER" || user.role === "ADMIN_NOT_BARMEN") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Button color={row.inMenu ? 'error' : 'success'} variant='contained'
|
||||||
|
onClick={() => handleEditMenu(row, !row.inMenu)}>
|
||||||
|
{(row.inMenu ? 'Удалить из' : 'Добавить в') + ' меню'}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
{/*<Loading loading={load}/>*/}
|
||||||
|
{/*Модальное окно информации о коктейле*/}
|
||||||
|
<CocktailInfoModal row={selectedCocktail} open={open}
|
||||||
|
closeHandler={handleCloseCocktailModal}/>
|
||||||
|
{/*Блок фильтров*/}
|
||||||
|
<FilterBlock
|
||||||
|
filter={filter}
|
||||||
|
handleFilterChange={handleFilterChange}
|
||||||
|
handleClearFilter={handleFilterClear}
|
||||||
|
barmen={user.role !== 'USER'}
|
||||||
|
all={all}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/*todo: доделать фильтр по количеству недостающих ингредиентов*/}
|
||||||
|
{/*{*/}
|
||||||
|
{/* (all && filter.iCount === 1) && (*/}
|
||||||
|
{/* <Paper sx={{mt: 1}}>*/}
|
||||||
|
{/* <CheckMarks rows={chips} name={"Выбор ингредиента"} filterName={"ingredient"}*/}
|
||||||
|
{/* filterValue={filter.ingredient}*/}
|
||||||
|
{/* handleChange={handleFilterChange}*/}
|
||||||
|
{/* identity*/}
|
||||||
|
{/* />*/}
|
||||||
|
{/* </Paper>*/}
|
||||||
|
{/* )*/}
|
||||||
|
{/*}*/}
|
||||||
|
<Box>
|
||||||
|
{/*Основное содержимое*/}
|
||||||
|
<Grid container rowSpacing={2} columnSpacing={{xs: 1, sm: 1, md: 2}} sx={{m: 1}}>
|
||||||
|
{rows.length > 0 && rows.map((row) => {
|
||||||
|
return (
|
||||||
|
<Cocktail key={row.id} row={row} handleFavourite={handleFavourite}
|
||||||
|
handleChangeRating={handleChangeRating}
|
||||||
|
handleSelect={handleSelectCocktail}
|
||||||
|
editMenuBlock={editMenuBlock}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
{load && renderSkeleton()}
|
||||||
|
{rows.length === 0 && (<NoResult/>)}
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
<Fab sx={{
|
||||||
|
alpha: '30%',
|
||||||
|
position: 'sticky',
|
||||||
|
left: 'calc(100% - 16px)',
|
||||||
|
bottom: '16px',
|
||||||
|
color: 'common.white',
|
||||||
|
bgcolor: blue[600],
|
||||||
|
'&:hover': {
|
||||||
|
bgcolor: blue[600],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
onClick={() => window.window.scrollTo(0, 0)}
|
||||||
|
aria-label='Expand'
|
||||||
|
color='inherit'>
|
||||||
|
<UpIcon/>
|
||||||
|
</Fab>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CocktailsPageContent;
|
||||||
258
front/src/app/pages/cocktails/EditCocktailPage.js
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import Toolbar from "@mui/material/Toolbar";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import * as React from "react";
|
||||||
|
import {useEffect, useState} from "react";
|
||||||
|
import Paper from "@mui/material/Paper";
|
||||||
|
import {Autocomplete} from "@mui/material";
|
||||||
|
import TextField from "@mui/material/TextField";
|
||||||
|
import {api} from "../../../lib/clients/api";
|
||||||
|
import {requests} from "../../../requests";
|
||||||
|
import {useAlert} from "../../../hooks/useAlert";
|
||||||
|
import Stack from "@mui/material/Stack";
|
||||||
|
import Button from "@mui/material/Button";
|
||||||
|
import CheckMarks from "../../../components/cocktails/CheckMarks";
|
||||||
|
import {EditCocktailReceipt} from "../../../components/cocktails/EditCocktailReceipt";
|
||||||
|
import {SelectEdit} from "../../../components/cocktails/SelectEdit";
|
||||||
|
import {getComparator} from "../../../components/core/getComparator";
|
||||||
|
import {useSearchParams} from "react-router-dom";
|
||||||
|
import {Loading} from "../../../components/core/Loading";
|
||||||
|
import CloudUploadIcon from '@mui/icons-material/CloudUpload';
|
||||||
|
import {styled} from "@mui/material/styles";
|
||||||
|
|
||||||
|
const emptyCocktail = {
|
||||||
|
id: null,
|
||||||
|
name: "",
|
||||||
|
alcoholic: "",
|
||||||
|
category: "",
|
||||||
|
components: "",
|
||||||
|
glass: "",
|
||||||
|
image: "",
|
||||||
|
instructions: "",
|
||||||
|
isAllowed: false,
|
||||||
|
rating: {
|
||||||
|
rating: 0,
|
||||||
|
favourite: false
|
||||||
|
},
|
||||||
|
receipt: [],
|
||||||
|
tags: "",
|
||||||
|
video: ""
|
||||||
|
};
|
||||||
|
const alcohol = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: "Алкогольный"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: "Безалкогольный",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: "Опционально"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
const VisuallyHiddenInput = styled('input')({
|
||||||
|
clip: 'rect(0 0 0 0)',
|
||||||
|
clipPath: 'inset(50%)',
|
||||||
|
height: 1,
|
||||||
|
overflow: 'hidden',
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
width: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
export function EditCocktailPage() {
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const {createError, createSuccess, getError} = useAlert();
|
||||||
|
const [cocktails, setCocktails] = useState([]);
|
||||||
|
const [selected, setSelected] = useState(null);
|
||||||
|
const [cocktail, setCocktail] = useState(emptyCocktail);
|
||||||
|
|
||||||
|
const [glass, setGlass] = useState([]);
|
||||||
|
const [category, setCategory] = useState([]);
|
||||||
|
const [tags, setTags] = useState([])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
api().get(requests.cocktails.simple)
|
||||||
|
.then((r) => {
|
||||||
|
const arr = r.data.sort(getComparator("asc", "name"));
|
||||||
|
setCocktails(arr)
|
||||||
|
|
||||||
|
const currentId = searchParams.get("id");
|
||||||
|
if (!currentId) {
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const currentCocktail = arr.find((r) => r.id === (currentId * 1));
|
||||||
|
if (!currentCocktail) {
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSelected(currentCocktail.id);
|
||||||
|
setLoading(false);
|
||||||
|
})
|
||||||
|
.catch(() => createError("Ошибка получения данных"))
|
||||||
|
|
||||||
|
api().get(requests.bar.category)
|
||||||
|
.then((r) => setCategory(r.data.sort(getComparator("asc", "name"))))
|
||||||
|
.catch(() => createError("Ошибка получения категорий"))
|
||||||
|
|
||||||
|
api().get(requests.bar.glass)
|
||||||
|
.then((r) => setGlass(r.data.sort(getComparator("asc", "name"))))
|
||||||
|
.catch(() => createError("Ошибка получения посуды"))
|
||||||
|
|
||||||
|
api().get(requests.bar.tags)
|
||||||
|
.then((r) => setTags(r.data.sort(getComparator("asc", "name"))))
|
||||||
|
.catch(() => createError("Ошибка получения тегов"))
|
||||||
|
// eslint-disable-next-line
|
||||||
|
}, []);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selected) {
|
||||||
|
setCocktail(emptyCocktail);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
api().get(requests.cocktails.cocktail + selected)
|
||||||
|
.then((r) => {
|
||||||
|
setCocktail(r.data)
|
||||||
|
})
|
||||||
|
.catch(() => getError());
|
||||||
|
// eslint-disable-next-line
|
||||||
|
}, [selected])
|
||||||
|
|
||||||
|
const changeCocktailValue = (name, value) => {
|
||||||
|
if (name === "tags") {
|
||||||
|
value = value.join(",");
|
||||||
|
}
|
||||||
|
setCocktail((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[name]: value
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
const saveHandler = () => {
|
||||||
|
api().patch(requests.cocktails.edit, cocktail)
|
||||||
|
.then((r) => {
|
||||||
|
if (!r.data.error) {
|
||||||
|
createSuccess("Сохранено")
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
createError("Ошибка на сервере: " + r.data.error)
|
||||||
|
})
|
||||||
|
.catch(() => createError("Неизвестная ошибка"))
|
||||||
|
}
|
||||||
|
const deleteHandle = () => {
|
||||||
|
api().delete(requests.cocktails.cocktail + cocktail.id)
|
||||||
|
.then(() => {
|
||||||
|
setCocktails(cocktails.filter((r) => r.id !== cocktail.id))
|
||||||
|
setCocktail(emptyCocktail);
|
||||||
|
})
|
||||||
|
.catch(() => createError("Ошибка удаления коктейля"))
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
{/*Загрузка*/}
|
||||||
|
<Loading loading={loading}/>
|
||||||
|
{/*Заголовок*/}
|
||||||
|
<Toolbar>
|
||||||
|
<Typography variant="h6" component="div" sx={{flexGrow: 1}}>Коктейли</Typography>
|
||||||
|
</Toolbar>
|
||||||
|
{/*Поиск*/}
|
||||||
|
<Paper elevation={6} sx={{my: 2, display: 'grid', p: 2}}>
|
||||||
|
<Autocomplete
|
||||||
|
disablePortal
|
||||||
|
options={cocktails}
|
||||||
|
onChange={(e, v) => {
|
||||||
|
if (!v) {
|
||||||
|
setCocktail(emptyCocktail);
|
||||||
|
setSelected(null)
|
||||||
|
} else {
|
||||||
|
setSelected(v.id)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
isOptionEqualToValue={(selected, value) => selected.id === value.id}
|
||||||
|
getOptionKey={(selected) => selected.id}
|
||||||
|
getOptionLabel={(selected) => selected.name + (selected.hasError ? " (есть ошибка)" : "")}
|
||||||
|
renderInput={(params) => <TextField {...params} label="Поиск"/>}
|
||||||
|
/>
|
||||||
|
</Paper>
|
||||||
|
{/*Рабочая область*/}
|
||||||
|
<Paper elevation={6} sx={{my: 2, display: 'grid', p: 1, pb: 2}}>
|
||||||
|
<Stack>
|
||||||
|
<Box hidden={cocktail.id === null} ml={1} mb={1}>
|
||||||
|
<Button color='error' onClick={() => deleteHandle()}>Удалить коктейль</Button>
|
||||||
|
</Box>
|
||||||
|
{/*Фото*/}
|
||||||
|
<Box ml={1}>
|
||||||
|
<img src={cocktail.image} alt={""} width={300} height={300} loading={'eager'}/>
|
||||||
|
</Box>
|
||||||
|
{/*Редактирование ссылки на фото*/}
|
||||||
|
<Stack direction='row' pr={2} m={1} display='relative'>
|
||||||
|
<TextField sx={{width: '75%'}}
|
||||||
|
label={"Ссылка на фото"} variant='outlined' multiline
|
||||||
|
value={!cocktail.image ? "" : cocktail.image}
|
||||||
|
onChange={(e) => changeCocktailValue("image", e.target.value)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
component="label"
|
||||||
|
role={undefined}
|
||||||
|
variant="contained"
|
||||||
|
tabIndex={-1}
|
||||||
|
startIcon={<CloudUploadIcon/>}
|
||||||
|
sx={{width: '10%', fontSize: 40, ml: 1, pr: 1}}
|
||||||
|
>
|
||||||
|
<VisuallyHiddenInput
|
||||||
|
type="file"
|
||||||
|
accept=".jpg,.jpeg,.png"
|
||||||
|
onChange={(event) => {
|
||||||
|
const file = event.target.files[0];
|
||||||
|
let formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
api().post(requests.cocktails.savePhoto, formData)
|
||||||
|
.then((r) => changeCocktailValue("image", r.data))
|
||||||
|
.catch(() => getError())
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
</Stack>
|
||||||
|
{/*Название*/}
|
||||||
|
<Box m={1}>
|
||||||
|
<TextField sx={{mr: 1, mb: 2, minWidth: 300}}
|
||||||
|
variant="outlined" label={"Название"}
|
||||||
|
value={cocktail.name}
|
||||||
|
onChange={(e) => changeCocktailValue("name", e.target.value)}/>
|
||||||
|
</Box>
|
||||||
|
{/*Категория, посуда, алкогольность, теги*/}
|
||||||
|
<Box mb={2}>
|
||||||
|
<SelectEdit value={cocktail.category} label={"Категория"} width={300} margin={1}
|
||||||
|
array={category}
|
||||||
|
attributeName={"category"} handler={changeCocktailValue}/>
|
||||||
|
<SelectEdit value={cocktail.glass} label={"Посуда"} width={300} margin={1} array={glass}
|
||||||
|
attributeName={"glass"} handler={changeCocktailValue}/>
|
||||||
|
<SelectEdit value={cocktail.alcoholic} label={"Алкогольность"} width={300} margin={1}
|
||||||
|
array={alcohol}
|
||||||
|
attributeName={"alcoholic"} handler={changeCocktailValue}/>
|
||||||
|
<CheckMarks rows={tags} width={300} name={"Теги"} handleChange={changeCocktailValue}
|
||||||
|
filterValue={cocktail.tags.split(",")} filterName={"tags"}/>
|
||||||
|
</Box>
|
||||||
|
{/*Рецепт*/}
|
||||||
|
<EditCocktailReceipt receipt={cocktail.receipt} handler={changeCocktailValue}/>
|
||||||
|
|
||||||
|
<Box pr={2} ml={1}>
|
||||||
|
<TextField sx={{width: '100%'}}
|
||||||
|
label={"Инструкция"} variant='outlined' multiline
|
||||||
|
value={!cocktail.instructions ? "" : cocktail.instructions}
|
||||||
|
onChange={(e) => changeCocktailValue("instructions", e.target.value)}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
<Box display={'flex'} justifyContent={'flex-end'}>
|
||||||
|
<Button variant='contained' onClick={() => saveHandler()}>Сохранить</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
7
front/src/app/pages/cocktails/MenuPage.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import CocktailsPageContent from "./CocktailsPageContent";
|
||||||
|
|
||||||
|
export function MenuPage() {
|
||||||
|
return (
|
||||||
|
<CocktailsPageContent all={false}/>
|
||||||
|
)
|
||||||
|
}
|
||||||
169
front/src/app/pages/ingredients/EditIngredientPage.js
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import Toolbar from "@mui/material/Toolbar";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import * as React from "react";
|
||||||
|
import {useEffect, useState} from "react";
|
||||||
|
import Paper from "@mui/material/Paper";
|
||||||
|
import {Autocomplete, FormControl, FormControlLabel, InputLabel} from "@mui/material";
|
||||||
|
import {api} from "../../../lib/clients/api";
|
||||||
|
import {requests} from "../../../requests";
|
||||||
|
import {useAlert} from "../../../hooks/useAlert";
|
||||||
|
import {useSearchParams} from "react-router-dom";
|
||||||
|
import TextField from "@mui/material/TextField";
|
||||||
|
import Switch from "@mui/material/Switch";
|
||||||
|
import Stack from "@mui/material/Stack";
|
||||||
|
import Button from "@mui/material/Button";
|
||||||
|
import Select from "@mui/material/Select";
|
||||||
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
|
import {getComparator} from "../../../components/core/getComparator";
|
||||||
|
|
||||||
|
const emptyIngredient = {
|
||||||
|
id: null,
|
||||||
|
name: "",
|
||||||
|
enName: "",
|
||||||
|
have: false,
|
||||||
|
image: null,
|
||||||
|
type: "",
|
||||||
|
alcohol: false,
|
||||||
|
abv: null,
|
||||||
|
description: null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EditIngredientPage() {
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const [ingredients, setIngredients] = useState([]);
|
||||||
|
const [types, setTypes] = useState([]);
|
||||||
|
const [ingredient, setIngredient] = useState(emptyIngredient)
|
||||||
|
const {createError, createSuccess} = useAlert();
|
||||||
|
useEffect(() => {
|
||||||
|
api().get(requests.bar.ingredientList)
|
||||||
|
.then((r) => {
|
||||||
|
const arr = r.data.sort(getComparator("asc", "name"));
|
||||||
|
setIngredients(arr)
|
||||||
|
|
||||||
|
const currentId = searchParams.get("id");
|
||||||
|
if (!currentId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const currentIngredient = arr.find((r) => r.id === (currentId * 1));
|
||||||
|
if (!currentIngredient) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIngredient(currentIngredient);
|
||||||
|
})
|
||||||
|
.catch(() => createError("Ошибка получения данных"))
|
||||||
|
|
||||||
|
api().get(requests.bar.type)
|
||||||
|
.then((r) => setTypes(r.data.sort(getComparator("asc", "name"))))
|
||||||
|
// eslint-disable-next-line
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const changeIngredientValue = (name, value) => {
|
||||||
|
setIngredient((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[name]: value
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
const saveIngredientHandler = () => {
|
||||||
|
api().patch(requests.bar.ingredient, ingredient)
|
||||||
|
.then(() => createSuccess("Ингредиент сохранен"))
|
||||||
|
.catch(() => createError("Ошибка сохранения"))
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
{/*Заголовок*/}
|
||||||
|
<Toolbar>
|
||||||
|
<Typography variant="h6" component="div" sx={{flexGrow: 1}}>Ингредиенты</Typography>
|
||||||
|
</Toolbar>
|
||||||
|
{/*Поиск*/}
|
||||||
|
<Paper elevation={6} sx={{my: 2, display: 'grid', p: 2}}>
|
||||||
|
<Autocomplete
|
||||||
|
disablePortal
|
||||||
|
options={ingredients}
|
||||||
|
|
||||||
|
defaultChecked={emptyIngredient}
|
||||||
|
onChange={(e, v) => {
|
||||||
|
console.log(v);
|
||||||
|
return !v ? setIngredient(emptyIngredient) : setIngredient(v)
|
||||||
|
}}
|
||||||
|
isOptionEqualToValue={(selected, value) => selected.id === value.id}
|
||||||
|
getOptionKey={(selected) => selected.id}
|
||||||
|
getOptionLabel={(selected) => selected.name}
|
||||||
|
renderInput={(params) => <TextField {...params} label="Ингредиенты"/>}
|
||||||
|
/>
|
||||||
|
</Paper>
|
||||||
|
{/*Форма ингредиента*/}
|
||||||
|
<Paper elevation={6} sx={{my: 2, display: 'grid', p: 1, pb: 2}}>
|
||||||
|
<Stack>
|
||||||
|
<Box display={'flex'} justifyContent={'flex-end'} pr={2}>
|
||||||
|
<FormControlLabel control={
|
||||||
|
<Switch
|
||||||
|
checked={ingredient.have}
|
||||||
|
onChange={() => changeIngredientValue("have", !ingredient.have)}
|
||||||
|
/>}
|
||||||
|
label={"Наличие"} labelPlacement={'start'}/>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<img src={ingredient.image} alt={""} loading={'eager'}/>
|
||||||
|
</Box>
|
||||||
|
<Box m={1}>
|
||||||
|
<TextField sx={{mr: 1, mb: 2, minWidth: 330}}
|
||||||
|
variant="outlined" label={"Название"}
|
||||||
|
value={ingredient.name}
|
||||||
|
onChange={(e) => changeIngredientValue("name", e.target.value)}/>
|
||||||
|
<TextField sx={{mr: 1, mb: 2, minWidth: 330}}
|
||||||
|
label="Английское название" variant="outlined"
|
||||||
|
value={ingredient.enName}
|
||||||
|
onChange={(e) => changeIngredientValue("enName", e.target.value)}/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box height={70} mt={1} ml={1}>
|
||||||
|
<FormControlLabel sx={{pt: 1}}
|
||||||
|
control={
|
||||||
|
<Switch
|
||||||
|
checked={ingredient.alcohol}
|
||||||
|
onChange={() => changeIngredientValue("alcohol", !ingredient.alcohol)}
|
||||||
|
/>}
|
||||||
|
label="Алкогольный"/>
|
||||||
|
{ingredient.alcohol && (
|
||||||
|
<TextField sx={{width: 100}}
|
||||||
|
variant='outlined' label='Градус'
|
||||||
|
value={!ingredient.abv ? "" : ingredient.abv}
|
||||||
|
onChange={(e) => changeIngredientValue("abv", e.target.value)}/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box mb={2} ml={1}>
|
||||||
|
<FormControl sx={{width: 330}}>
|
||||||
|
<InputLabel id="select-label">Категория</InputLabel>
|
||||||
|
<Select
|
||||||
|
id="select-label"
|
||||||
|
autoWidth
|
||||||
|
label={"Категория"}
|
||||||
|
value={!ingredient.type ? "" : ingredient.type}
|
||||||
|
onChange={(e) => changeIngredientValue("type", e.target.value)}
|
||||||
|
>
|
||||||
|
<MenuItem value="">
|
||||||
|
<em>None</em>
|
||||||
|
</MenuItem>
|
||||||
|
{types.map((c) => {
|
||||||
|
return (<MenuItem key={c.id} value={c.name}>{c.name}</MenuItem>)
|
||||||
|
})}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box pr={2} ml={1}>
|
||||||
|
<TextField sx={{width: '100%'}}
|
||||||
|
label={"Описание"} variant='outlined' multiline
|
||||||
|
value={!ingredient.description ? "" : ingredient.description}/>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
<Box display={'flex'} justifyContent={'flex-end'}>
|
||||||
|
<Button variant='contained' onClick={() => saveIngredientHandler()}>Сохранить</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
153
front/src/app/pages/ingredients/IngredientsPage.js
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import Toolbar from "@mui/material/Toolbar";
|
||||||
|
import Paper from "@mui/material/Paper";
|
||||||
|
import {Fab, FormControl, FormControlLabel, InputAdornment, InputLabel, OutlinedInput, Tabs} from "@mui/material";
|
||||||
|
import IconButton from "@mui/material/IconButton";
|
||||||
|
import SearchIcon from "@mui/icons-material/Search";
|
||||||
|
import * as React from "react";
|
||||||
|
import {useEffect, useMemo, useState} from "react";
|
||||||
|
import {Loading} from "../../../components/core/Loading";
|
||||||
|
import {requests} from "../../../requests";
|
||||||
|
import {useAlert} from "../../../hooks/useAlert";
|
||||||
|
import {IngredientInfoModal} from "../../../components/Ingredients/IngredientInfoModal";
|
||||||
|
import {api} from "../../../lib/clients/api";
|
||||||
|
import Tab from "@mui/material/Tab";
|
||||||
|
import {a11yProps} from "../../../components/core/tabProps";
|
||||||
|
import {CustomTabPanel} from "../../../components/core/TabPanel";
|
||||||
|
import {IngredientList} from "../../../components/Ingredients/IngredientList";
|
||||||
|
import {blue} from "@mui/material/colors";
|
||||||
|
import UpIcon from "@mui/icons-material/KeyboardArrowUp";
|
||||||
|
import Switch from "@mui/material/Switch";
|
||||||
|
|
||||||
|
export function IngredientsPage() {
|
||||||
|
const [value, setValue] = React.useState(0);
|
||||||
|
const [grouping, setGrouping] = useState(true);
|
||||||
|
const handleChange = (event, newValue) => setValue(newValue);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [findString, setFindString] = useState("");
|
||||||
|
const [ingredients, setIngredients] = useState([]);
|
||||||
|
const [openModal, setOpenModal] = useState(false);
|
||||||
|
const [selectedInfo, setSelectedInfo] = useState(null);
|
||||||
|
const {createError} = useAlert();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
api().get(requests.bar.ingredientList)
|
||||||
|
.then((r) => {
|
||||||
|
setIngredients(r.data)
|
||||||
|
setLoading(false);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
createError("Ошибка получения списка ингредиентов");
|
||||||
|
setLoading(false);
|
||||||
|
})
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const visibleIngredients = useMemo(() => {
|
||||||
|
if (findString.length === 0) {
|
||||||
|
return ingredients;
|
||||||
|
}
|
||||||
|
const reg = new RegExp("(.*?)" + findString + "(.*?)", "i");
|
||||||
|
return ingredients.filter((ingredient) => ingredient.name.match(reg) !== null);
|
||||||
|
}, [findString, ingredients]);
|
||||||
|
const ingredientsToAdd = visibleIngredients.filter((ingredient) => !ingredient.have);
|
||||||
|
const ingredientsInBar = visibleIngredients.filter((ingredient) => ingredient.have);
|
||||||
|
|
||||||
|
const changeHandler = (row, value) => {
|
||||||
|
const newState = ingredients.map((ingredient) => {
|
||||||
|
if (ingredient.id === row.id) {
|
||||||
|
return {
|
||||||
|
...ingredient,
|
||||||
|
have: value
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return ingredient;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const url = `${requests.bar.ingredient}?id=${row.id}`;
|
||||||
|
const request = value ? api().put(url) : api().delete(url);
|
||||||
|
request
|
||||||
|
.then(() => {
|
||||||
|
setIngredients(newState);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
createError("Ошибка изменения ингредиента");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const handleOpenModal = (i) => {
|
||||||
|
setOpenModal(true);
|
||||||
|
setSelectedInfo(i);
|
||||||
|
}
|
||||||
|
const handleCloseModal = () => {
|
||||||
|
setSelectedInfo(null);
|
||||||
|
setOpenModal(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
{/*Заголовок*/}
|
||||||
|
<Toolbar>
|
||||||
|
<Typography variant="h6" component="div" sx={{flexGrow: 1}}>Ингредиенты бара</Typography>
|
||||||
|
</Toolbar>
|
||||||
|
{/*Поиск*/}
|
||||||
|
<Paper elevation={6} sx={{my: 2}}>
|
||||||
|
<FormControl sx={{m: 1, width: 'calc(100% - 20px'}}>
|
||||||
|
<InputLabel htmlFor="outlined-adornment-amount">Поиск</InputLabel>
|
||||||
|
<OutlinedInput
|
||||||
|
onChange={(e) => setFindString(e.target.value)}
|
||||||
|
label="With normal TextField"
|
||||||
|
startAdornment={
|
||||||
|
<InputAdornment position="start">
|
||||||
|
<IconButton edge="end">
|
||||||
|
<SearchIcon/>
|
||||||
|
</IconButton>
|
||||||
|
</InputAdornment>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormControlLabel sx={{ml: '2px'}}
|
||||||
|
control={<Switch defaultChecked/>}
|
||||||
|
onClick={() => setGrouping(!grouping)}
|
||||||
|
label="Группировать"
|
||||||
|
labelPlacement="end"/>
|
||||||
|
</Paper>
|
||||||
|
{/*Рабочее поле ингредиентов*/}
|
||||||
|
<Box>
|
||||||
|
<Tabs value={value} onChange={handleChange} aria-label="basic tabs example">
|
||||||
|
<Tab label="В баре" {...a11yProps(0)} />
|
||||||
|
<Tab label="Список" {...a11yProps(1)} />
|
||||||
|
</Tabs>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<CustomTabPanel value={value} index={0}>
|
||||||
|
<IngredientList rows={ingredientsInBar} value={false} changeHandler={changeHandler}
|
||||||
|
infoHandler={handleOpenModal} grouping={grouping}/>
|
||||||
|
</CustomTabPanel>
|
||||||
|
<CustomTabPanel value={value} index={1}>
|
||||||
|
<IngredientList rows={ingredientsToAdd} value={true} changeHandler={changeHandler}
|
||||||
|
infoHandler={handleOpenModal} grouping={grouping}/>
|
||||||
|
</CustomTabPanel>
|
||||||
|
</Box>
|
||||||
|
<Fab sx={{
|
||||||
|
alpha: '30%',
|
||||||
|
position: 'sticky',
|
||||||
|
bottom: '16px',
|
||||||
|
color: 'common.white',
|
||||||
|
bgcolor: blue[600],
|
||||||
|
'&:hover': {
|
||||||
|
bgcolor: blue[600],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
onClick={() => window.window.scrollTo(0, 0)}
|
||||||
|
aria-label='Expand'
|
||||||
|
color='inherit'>
|
||||||
|
<UpIcon/>
|
||||||
|
</Fab>
|
||||||
|
{/*Загрузчик*/}
|
||||||
|
<Loading loading={loading}/>
|
||||||
|
{/*Модальное окно информации об ингредиенте*/}
|
||||||
|
<IngredientInfoModal ingredient={selectedInfo} open={openModal} closeHandler={handleCloseModal}/>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
38
front/src/app/pages/notFound/NotFoundPage.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import Stack from '@mui/material/Stack';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft';
|
||||||
|
import {paths} from "../../../path";
|
||||||
|
|
||||||
|
export default function NotFoundPage() {
|
||||||
|
return (
|
||||||
|
<Box component="main" sx={{ alignItems: 'center', display: 'flex', justifyContent: 'center', minHeight: '100%' }}>
|
||||||
|
<Stack spacing={3} sx={{ alignItems: 'center', maxWidth: 'md' }}>
|
||||||
|
<Box>
|
||||||
|
<Box
|
||||||
|
component="img"
|
||||||
|
alt="Under development"
|
||||||
|
src="/assets/error-404.png"
|
||||||
|
sx={{ display: 'inline-block', height: 'auto', maxWidth: '100%', width: '400px' }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Typography variant="h3" sx={{ textAlign: 'center' }}>
|
||||||
|
404: Страница не найдена или недоступна
|
||||||
|
</Typography>
|
||||||
|
<Typography color="text.secondary" variant="body1" sx={{ textAlign: 'center' }}>
|
||||||
|
Вы либо выбрали какой-то сомнительный маршрут, либо попали сюда по ошибке. Что бы это ни было, попробуйте воспользоваться навигацией
|
||||||
|
</Typography>
|
||||||
|
<Button
|
||||||
|
// component={'a'}
|
||||||
|
href={paths.home}
|
||||||
|
startIcon={<ArrowLeftIcon fontSize="var(--icon-fontSize-md)" />}
|
||||||
|
variant="contained"
|
||||||
|
>
|
||||||
|
На домашнюю страницу
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
7
front/src/app/pages/queue/MyQueuePage.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import {QueueContent} from "./QueueContent";
|
||||||
|
|
||||||
|
export function MyQueuePage() {
|
||||||
|
return (
|
||||||
|
<QueueContent my={true}/>
|
||||||
|
)
|
||||||
|
}
|
||||||
82
front/src/app/pages/queue/QueueContent.js
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import {useEffect, useMemo, useState} from "react";
|
||||||
|
import {useAlert} from "../../../hooks/useAlert";
|
||||||
|
import * as React from "react";
|
||||||
|
import {api} from "../../../lib/clients/api";
|
||||||
|
import {requests} from "../../../requests";
|
||||||
|
import {createHeadCell} from "../../../components/orders/createHeadCelll";
|
||||||
|
import {Loading} from "../../../components/core/Loading";
|
||||||
|
import OrderModal from "../../../components/orders/OrderModal";
|
||||||
|
import EnhancedTable from "../../../components/orders/EnhancedTable";
|
||||||
|
|
||||||
|
export function QueueContent({my}) {
|
||||||
|
const [load, setLoad] = useState(false);
|
||||||
|
const [orders, setOrders] = useState([]);
|
||||||
|
const {createSuccess, createError} = useAlert();
|
||||||
|
const [openModal, setOpenModal] = React.useState(false);
|
||||||
|
const [selected, setSelected] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLoad(false);
|
||||||
|
const url = my ? requests.bar.myOrders : requests.bar.order;
|
||||||
|
api().get(url)
|
||||||
|
.then(r => {
|
||||||
|
setOrders(r.data);
|
||||||
|
setLoad(true);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
createError("Ошибка при получении заказов");
|
||||||
|
setLoad(true)
|
||||||
|
})
|
||||||
|
// eslint-disable-next-line
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const sliced = useMemo(() => orders.sort((a, b) => b.id - a.id), [orders])
|
||||||
|
|
||||||
|
const cells = [
|
||||||
|
createHeadCell('id', true, true, 'Номер заказа', "20px"),
|
||||||
|
createHeadCell('cocktail.name', true, false, 'Коктейль', "40px"),
|
||||||
|
createHeadCell('visitor.name', true, false, 'Клиент', "40px"),
|
||||||
|
createHeadCell('status', true, true, 'Статус', "30px"),
|
||||||
|
];
|
||||||
|
|
||||||
|
const changeOrderHandle = (row, status) => {
|
||||||
|
let url = requests.bar.order + "?id=" + row.id;
|
||||||
|
let isCancel = status === "CANCEL";
|
||||||
|
let request = isCancel ? api().delete(url) : api().put(url);
|
||||||
|
request
|
||||||
|
.then(() => {
|
||||||
|
createSuccess(isCancel ? "Заказ отменен" : "Заказ готов");
|
||||||
|
let newArr = orders.filter((order) => {
|
||||||
|
if (order.id !== row.id) {
|
||||||
|
row.status = isCancel ? "CANCEL" : "DONE";
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
return order;
|
||||||
|
})
|
||||||
|
setOrders(newArr);
|
||||||
|
setSelected(null);
|
||||||
|
setOpenModal(false);
|
||||||
|
})
|
||||||
|
.catch(() => createError("Ошибка изменения заказа"))
|
||||||
|
}
|
||||||
|
const handleSelect = (row) => {
|
||||||
|
setSelected(row);
|
||||||
|
setOpenModal(true);
|
||||||
|
}
|
||||||
|
const handleCloseModal = () => {
|
||||||
|
setOpenModal(false);
|
||||||
|
setSelected(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterValues = !my ? ["DONE", "CANCEL"] : [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Loading loading={!load}/>
|
||||||
|
<OrderModal row={selected} handleClose={handleCloseModal} open={openModal}
|
||||||
|
handleChange={changeOrderHandle} my={my}/>
|
||||||
|
<EnhancedTable name={"Заказы"} rows={sliced} cells={cells} handleSelect={handleSelect} filterEqual={false}
|
||||||
|
filterField={["status"]} filterValue={filterValues}/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
10
front/src/app/pages/queue/QueuePage.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import {QueueContent} from "./QueueContent";
|
||||||
|
|
||||||
|
const QueuePage = () => {
|
||||||
|
return (
|
||||||
|
<QueueContent my={false}/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default QueuePage;
|
||||||
37
front/src/components/Ingredients/IngredientCard.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import {Card} from "@mui/material";
|
||||||
|
import IconButton from "@mui/material/IconButton";
|
||||||
|
import DeleteIcon from '@mui/icons-material/Delete';
|
||||||
|
import AddBoxRoundedIcon from '@mui/icons-material/AddBoxRounded';
|
||||||
|
import InfoRoundedIcon from '@mui/icons-material/InfoRounded';
|
||||||
|
import React from "react";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import Stack from "@mui/material/Stack";
|
||||||
|
import {paths} from "../../path";
|
||||||
|
import EditIcon from '@mui/icons-material/Edit';
|
||||||
|
|
||||||
|
export function IngredientCard({row, value, infoHandler, changeHandler}) {
|
||||||
|
return (
|
||||||
|
<Card sx={{mb: 1, height: '130px', display: 'relative', pt: 1}}>
|
||||||
|
<Stack direction='row' justifyContent='start' alignItems='center'>
|
||||||
|
<Box sx={{width: '100px', height: '100px'}}>
|
||||||
|
<img src={row.image} loading='eager' height={'100px'} width={'100px'} alt={row.id}/>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{width: 'calc(90% - 100px)', pr: 2}}>{row.name}</Box>
|
||||||
|
<Stack direction='row'>
|
||||||
|
<Box mr={1} pt={'3px'}>{!row.alcohol ? "" : `${row.abv}%`}</Box>
|
||||||
|
<Stack sx={{width: '5%'}} spacing={1} justifyContent='flex-start'>
|
||||||
|
<IconButton size='small' onClick={() => changeHandler(row, value)}>
|
||||||
|
{value ? <AddBoxRoundedIcon/> : <DeleteIcon/>}
|
||||||
|
</IconButton>
|
||||||
|
<IconButton size='small' onClick={() => infoHandler(row)}>
|
||||||
|
<InfoRoundedIcon/>
|
||||||
|
</IconButton>
|
||||||
|
<IconButton size='small' href={`${paths.bar.ingredientEdit}?id=${row.id}`}>
|
||||||
|
<EditIcon/>
|
||||||
|
</IconButton>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
37
front/src/components/Ingredients/IngredientInfoModal.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import Dialog from "@mui/material/Dialog";
|
||||||
|
import DialogTitle from "@mui/material/DialogTitle";
|
||||||
|
import DialogContent from "@mui/material/DialogContent";
|
||||||
|
import DialogActions from "@mui/material/DialogActions";
|
||||||
|
import Button from "@mui/material/Button";
|
||||||
|
import * as React from "react";
|
||||||
|
import Stack from "@mui/material/Stack";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
|
||||||
|
export function IngredientInfoModal({ingredient, open, closeHandler}) {
|
||||||
|
if (!ingredient) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Dialog fullWidth={true} maxWidth="350px" open={open} onClose={closeHandler}
|
||||||
|
sx={{
|
||||||
|
'& .MuiDialog-paper': {
|
||||||
|
margin: '8px',
|
||||||
|
},
|
||||||
|
'& .MuiPaper-root': {
|
||||||
|
width: 'calc(100% - 16px)',
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<DialogTitle>{ingredient.name}</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<Stack spacing={2} sx={{justifyContent: "center"}}>
|
||||||
|
<img src={ingredient.image} alt={ingredient.name} loading={"eager"} width={"300"}/>
|
||||||
|
{ingredient.alcohol && (<Typography>{`Крепость ${ingredient.abv}`}</Typography>)}
|
||||||
|
<Typography>{ingredient.description}</Typography>
|
||||||
|
</Stack>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={closeHandler}>Close</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
75
front/src/components/Ingredients/IngredientList.js
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import {IngredientCard} from "./IngredientCard";
|
||||||
|
import {useMemo, useState} from "react";
|
||||||
|
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||||
|
import {Accordion, AccordionDetails, AccordionSummary} from "@mui/material";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import {getComparator} from "../core/getComparator";
|
||||||
|
import {groupByForLoop} from "../core/groupByForLoop";
|
||||||
|
|
||||||
|
export function IngredientList({rows, value, infoHandler, changeHandler, grouping}) {
|
||||||
|
const [size, setSize] = useState(10);
|
||||||
|
window.addEventListener('scroll', () => {
|
||||||
|
if (window.innerHeight + window.scrollY >= (document.documentElement.scrollHeight - 100)) {
|
||||||
|
if (!grouping) {
|
||||||
|
setSize(size + 10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const visibleRows = useMemo(() => {
|
||||||
|
let res = [];
|
||||||
|
if (rows.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!grouping) {
|
||||||
|
return rows
|
||||||
|
.sort(getComparator("asc", "name"))
|
||||||
|
.slice(0, size)
|
||||||
|
.map((row) => {
|
||||||
|
return (
|
||||||
|
<IngredientCard row={row} key={row.id} value={value}
|
||||||
|
changeHandler={changeHandler} infoHandler={infoHandler}/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const group = groupByForLoop(rows, "type")
|
||||||
|
if (!group || group.size === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const keys = Array.from(group.keys());
|
||||||
|
keys.sort(getComparator("asc"))
|
||||||
|
.forEach((key) => {
|
||||||
|
const list = group.get(key)
|
||||||
|
res.push(
|
||||||
|
<Accordion key={key}>
|
||||||
|
<AccordionSummary
|
||||||
|
expandIcon={<ExpandMoreIcon/>}
|
||||||
|
aria-controls="panel1-content"
|
||||||
|
id="panel1-header"
|
||||||
|
>
|
||||||
|
<Typography component="span">{key !== "null" ? key : "Без категории"}</Typography>
|
||||||
|
</AccordionSummary>
|
||||||
|
<AccordionDetails>
|
||||||
|
{list.sort(getComparator("asc", "name"))
|
||||||
|
.map((row) => {
|
||||||
|
return (
|
||||||
|
<IngredientCard row={row} key={row.id} value={value}
|
||||||
|
changeHandler={changeHandler} infoHandler={infoHandler}/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</AccordionDetails>
|
||||||
|
</Accordion>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
return res;
|
||||||
|
// eslint-disable-next-line
|
||||||
|
}, [size, rows])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box mt={2}>
|
||||||
|
{visibleRows}
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
48
front/src/components/auth/guest-guard.js
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import Alert from '@mui/material/Alert';
|
||||||
|
import {useAuth} from "../../hooks/useAuth";
|
||||||
|
import {logger} from "../../lib/DefaultLogger";
|
||||||
|
import {paths} from "../../path";
|
||||||
|
|
||||||
|
export function GuestGuard({ children }) {
|
||||||
|
const { auth, error, isLoading } = useAuth();
|
||||||
|
const [isChecking, setIsChecking] = React.useState(true);
|
||||||
|
|
||||||
|
const checkPermissions = async () => {
|
||||||
|
if (isLoading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
setIsChecking(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auth) {
|
||||||
|
logger.debug('[GuestGuard]: User is logged in, redirecting to dashboard');
|
||||||
|
window.location.replace(paths.dashboard.overview);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsChecking(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
checkPermissions().catch(() => {
|
||||||
|
// noop
|
||||||
|
});
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- Expected
|
||||||
|
}, [auth, error, isLoading]);
|
||||||
|
|
||||||
|
if (isChecking) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <Alert color="error">{error}</Alert>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <React.Fragment>{children}</React.Fragment>;
|
||||||
|
}
|
||||||
135
front/src/components/auth/sign-in-form.js
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import {useState} from 'react';
|
||||||
|
import Stack from '@mui/material/Stack';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import {paths} from "../../path";
|
||||||
|
import TextField from "@mui/material/TextField";
|
||||||
|
import TelegramIcon from '@mui/icons-material/Telegram';
|
||||||
|
import IconButton from "@mui/material/IconButton";
|
||||||
|
import Button from "@mui/material/Button";
|
||||||
|
import CircularProgress from "@mui/material/CircularProgress";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import {red} from "@mui/material/colors";
|
||||||
|
import {requests} from "../../requests";
|
||||||
|
import {useAuth} from "../../hooks/useAuth";
|
||||||
|
import {api} from "../../lib/clients/api";
|
||||||
|
|
||||||
|
const emptyRequest = {
|
||||||
|
byLogin: false,
|
||||||
|
code: "",
|
||||||
|
login: "",
|
||||||
|
password: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SignInForm() {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
const [request, setRequest] = useState(emptyRequest);
|
||||||
|
const [pass, setPass] = useState(false)
|
||||||
|
const {checkSession} = useAuth();
|
||||||
|
|
||||||
|
const buttonSx = {
|
||||||
|
minWidth: "300px",
|
||||||
|
...(error && {
|
||||||
|
bgcolor: red[500],
|
||||||
|
'&:hover': {
|
||||||
|
bgcolor: red[700],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
const handleButtonClick = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await api().post(requests.auth.login, request);
|
||||||
|
|
||||||
|
if (response.data.error) {
|
||||||
|
setError(response.data.error);
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.setItem("token", response.data.token);
|
||||||
|
|
||||||
|
await checkSession?.();
|
||||||
|
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
const renderByCode = () => {
|
||||||
|
return (
|
||||||
|
<Stack direction='row' mt={1}>
|
||||||
|
<IconButton href={paths.auth.bot} target="_blank" color='primary'>
|
||||||
|
<TelegramIcon/>
|
||||||
|
</IconButton>
|
||||||
|
|
||||||
|
<TextField value={request.code}
|
||||||
|
onChange={(e) => setRequest((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
code: e.target.value
|
||||||
|
}))}
|
||||||
|
sx={{minWidth: 300}} id="outlined-basic" label="Код подтверждения"
|
||||||
|
variant="outlined"/>
|
||||||
|
</Stack>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const renderByLogin = () => {
|
||||||
|
return (
|
||||||
|
<Stack mt={1} spacing={2}>
|
||||||
|
<TextField value={request.login}
|
||||||
|
onChange={(e) => setRequest(prevState => ({
|
||||||
|
...prevState,
|
||||||
|
login: e.target.value
|
||||||
|
}))}
|
||||||
|
sx={{minWidth: 300}} id="loginField" label="Логин"
|
||||||
|
variant="outlined"/>
|
||||||
|
<TextField value={request.password}
|
||||||
|
onChange={(e) => setRequest((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
password: e.target.value
|
||||||
|
}))}
|
||||||
|
sx={{minWidth: 300}} id="passwordField" label="Пароль" type="password"
|
||||||
|
autoComplete="current-password" variant="outlined"/>
|
||||||
|
</Stack>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack spacing={4} sx={{marginBottom: '85%', marginTop: '45%'}}>
|
||||||
|
<Stack spacing={1}>
|
||||||
|
<Typography variant="h4">Авторизация</Typography>
|
||||||
|
<Typography variant='body1' component="a" href='#'
|
||||||
|
onClick={() => {
|
||||||
|
setRequest((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
byLogin: !pass
|
||||||
|
}))
|
||||||
|
setPass(!pass)
|
||||||
|
}}>
|
||||||
|
{pass ? "Вход по телеграмм-коду" : "Вход по логину и паролю"}
|
||||||
|
</Typography>
|
||||||
|
<Typography color="text.secondary" variant="body2">
|
||||||
|
{pass ? "Введите логин и пароль"
|
||||||
|
: "Для входа нужно всего лишь сказать об этом моему Telegram-боту, перейди по ссылке и набери \n/start"}
|
||||||
|
</Typography>
|
||||||
|
{pass ? renderByLogin() : renderByCode()}
|
||||||
|
<Box sx={{display: 'flex', alignItems: 'center', ml: '40px', mt: 1}}>
|
||||||
|
<Stack>
|
||||||
|
{error && (
|
||||||
|
<Typography mb={1} color={'error'}>{error}</Typography>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
sx={buttonSx}
|
||||||
|
disabled={loading}
|
||||||
|
onClick={handleButtonClick}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<CircularProgress
|
||||||
|
size={25}
|
||||||
|
/>
|
||||||
|
) : "Войти"}
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
40
front/src/components/bar/BarItem.js
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import {Card} from "@mui/material";
|
||||||
|
import Stack from "@mui/material/Stack";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import React from "react";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import Button from "@mui/material/Button";
|
||||||
|
|
||||||
|
const role = (myRole) => {
|
||||||
|
switch (myRole) {
|
||||||
|
case "ADMIN":
|
||||||
|
return "Администратор";
|
||||||
|
case "ADMIN_NOT_BARMEN":
|
||||||
|
return "Управляющий";
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BarItem({row, changeHandler, all, enterExist}) {
|
||||||
|
return (
|
||||||
|
<Card sx={{mb: 1, height: '100px', display: 'relative', p: 1}}>
|
||||||
|
<Stack direction='row' justifyContent='start' alignItems='start'>
|
||||||
|
<Box sx={{width: '75%', pr: 2}}>
|
||||||
|
<Typography variant='h6'>{row.name}</Typography>
|
||||||
|
<Typography>{role(row.myRole)}</Typography>
|
||||||
|
</Box>
|
||||||
|
<Stack sx={{width: '25%'}} spacing={1} justifyContent='flex-end' display='flex'>
|
||||||
|
<Typography color={row.open ? 'green' : 'red'}>{row.open ? "Бар открыт" : "Бар закрыт"}</Typography>
|
||||||
|
<Button variant='contained'
|
||||||
|
color={row.enter ? 'error' : 'success'}
|
||||||
|
onClick={() => changeHandler(row, !row.enter)}
|
||||||
|
disabled={!row.enter && enterExist}
|
||||||
|
>
|
||||||
|
{all ? "Добавить" : row.enter ? "Выйти" : "Войти"}
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
70
front/src/components/bar/BarList.js
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import {useEffect, useMemo, useState} from "react";
|
||||||
|
import {api} from "../../lib/clients/api";
|
||||||
|
import {requests} from "../../requests";
|
||||||
|
import {useAlert} from "../../hooks/useAlert";
|
||||||
|
import {BarItem} from "./BarItem";
|
||||||
|
import {Loading} from "../core/Loading";
|
||||||
|
import * as React from "react";
|
||||||
|
import {useUser} from "../../hooks/useUser";
|
||||||
|
|
||||||
|
export function BarList({all}) {
|
||||||
|
const {getError, createError} = useAlert();
|
||||||
|
const {refresh} = useUser();
|
||||||
|
const [bars, setBars] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLoading(true);
|
||||||
|
api().get(`${requests.bar.list}?my=${!all}`)
|
||||||
|
.then((r) => {
|
||||||
|
setBars(r.data)
|
||||||
|
setLoading(false);
|
||||||
|
})
|
||||||
|
.catch(() => getError())
|
||||||
|
// eslint-disable-next-line
|
||||||
|
}, []);
|
||||||
|
const enterExist = useMemo(() => bars.find((b) => b.enter), [bars])
|
||||||
|
const changeHandler = (row, value) => {
|
||||||
|
let request;
|
||||||
|
let newState;
|
||||||
|
if (!all) {
|
||||||
|
if (value && enterExist) {
|
||||||
|
//todo: добавить переключение
|
||||||
|
createError("Нельзя войти более чем в один бар одновременно")
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
request = api().patch(`${requests.bar.enter}${row.id}&value=${value}`);
|
||||||
|
newState = bars.map((b) => {
|
||||||
|
if (b.id !== row.id) {
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...b,
|
||||||
|
enter: value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
request = api().post(requests.bar.addToMyList, row);
|
||||||
|
newState = bars.filter((b) => b.id !== row.id);
|
||||||
|
}
|
||||||
|
request.then(() => {
|
||||||
|
setBars(newState)
|
||||||
|
refresh();
|
||||||
|
}).catch(() => getError())
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Box mt={2}>
|
||||||
|
{
|
||||||
|
bars.map((row) => {
|
||||||
|
return (
|
||||||
|
<BarItem key={row.id} row={row} changeHandler={changeHandler}
|
||||||
|
all={all} enterExist={enterExist}/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
{/*Загрузчик*/}
|
||||||
|
<Loading loading={loading}/>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
68
front/src/components/cocktails/CheckMarks.js
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import OutlinedInput from '@mui/material/OutlinedInput';
|
||||||
|
import InputLabel from '@mui/material/InputLabel';
|
||||||
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
|
import FormControl from '@mui/material/FormControl';
|
||||||
|
import ListItemText from '@mui/material/ListItemText';
|
||||||
|
import Select from '@mui/material/Select';
|
||||||
|
import Checkbox from '@mui/material/Checkbox';
|
||||||
|
|
||||||
|
const ITEM_HEIGHT = 48;
|
||||||
|
const ITEM_PADDING_TOP = 8;
|
||||||
|
const MenuProps = {
|
||||||
|
PaperProps: {
|
||||||
|
style: {
|
||||||
|
maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP,
|
||||||
|
width: 250,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function CheckMarks({rows, name, filterValue, handleChange, filterName, width, nonMulti, nullValue, identity}) {
|
||||||
|
const realValue = !nonMulti ? filterValue.filter((v) => v.length > 0) : filterValue;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<FormControl sx={{m: 1, width: !width ? 300 : width}}>
|
||||||
|
<InputLabel>{name}</InputLabel>
|
||||||
|
<Select
|
||||||
|
multiple={!nonMulti}
|
||||||
|
value={realValue}
|
||||||
|
onChange={(e) => handleChange(filterName, e.target.value)}
|
||||||
|
input={<OutlinedInput label={name}/>}
|
||||||
|
renderValue={(selected) => !nonMulti ? selected.join(", ") : selected}
|
||||||
|
MenuProps={MenuProps}
|
||||||
|
defaultChecked={nonMulti && rows[0]}
|
||||||
|
variant="filled">
|
||||||
|
{(nonMulti && nullValue) && (
|
||||||
|
<MenuItem value={""}>
|
||||||
|
<em>Не выбрано</em>
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
{rows.map((value) => {
|
||||||
|
if(identity) {
|
||||||
|
return (
|
||||||
|
<MenuItem key={"menuItemIn" + value} value={value}>
|
||||||
|
{!nonMulti && (
|
||||||
|
<Checkbox
|
||||||
|
checked={realValue.includes(value)}/>
|
||||||
|
)}
|
||||||
|
<ListItemText primary={value}/>
|
||||||
|
</MenuItem>
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<MenuItem key={value.id} value={value.name}>
|
||||||
|
{!nonMulti && (
|
||||||
|
<Checkbox
|
||||||
|
checked={realValue.includes(value.name)}/>
|
||||||
|
)}
|
||||||
|
<ListItemText primary={value.name}/>
|
||||||
|
</MenuItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
113
front/src/components/cocktails/Cocktail.js
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import {CardActions, CardContent, CardMedia, Rating} from "@mui/material";
|
||||||
|
import {useAlert} from "../../hooks/useAlert";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import List from "@mui/material/List";
|
||||||
|
import ListItem from "@mui/material/ListItem";
|
||||||
|
import ListItemText from "@mui/material/ListItemText";
|
||||||
|
import Button from "@mui/material/Button";
|
||||||
|
import Grid from "@mui/material/Grid";
|
||||||
|
import {requests} from "../../requests";
|
||||||
|
import {CocktailItemStyled} from "./CocktailItemStyled";
|
||||||
|
import IconButton from "@mui/material/IconButton";
|
||||||
|
import FavoriteBorderIcon from '@mui/icons-material/FavoriteTwoTone';
|
||||||
|
import FavoriteIcon from '@mui/icons-material/Favorite';
|
||||||
|
import {api} from "../../lib/clients/api";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import {useUser} from "../../hooks/useUser";
|
||||||
|
|
||||||
|
function renderFavouriteBadge(handleFavourite, row) {
|
||||||
|
const childIcon = row.rating.favourite ? <FavoriteIcon color='error'/> : <FavoriteBorderIcon color={'warning'}/>;
|
||||||
|
return (
|
||||||
|
<IconButton sx={{position: 'absolute', top: "15px", right: "15px"}} onClick={() => handleFavourite(row)}>
|
||||||
|
{childIcon}
|
||||||
|
</IconButton>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderRating(handleChangeRating, row) {
|
||||||
|
return (
|
||||||
|
<Rating
|
||||||
|
sx={{position: 'absolute', top: '310px', right: '85px'}}
|
||||||
|
name="simple-controlled"
|
||||||
|
size="large"
|
||||||
|
value={row.rating.rating}
|
||||||
|
onChange={(event, newValue) => handleChangeRating(row, newValue)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Cocktail({row, handleFavourite, handleChangeRating, handleSelect, editMenuBlock}) {
|
||||||
|
const {createAlert, createError} = useAlert();
|
||||||
|
const {session, user} = useUser();
|
||||||
|
|
||||||
|
function pay(cocktailId) {
|
||||||
|
api().post(`${requests.bar.pay}cocktail=${cocktailId}`)
|
||||||
|
.then(() => createAlert("Ожидайте свой заказ", "success"))
|
||||||
|
.catch(() => createError("Ошибка во время создания заказа"))
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid item sx={{pr: 2}}>
|
||||||
|
<CocktailItemStyled>
|
||||||
|
<Box sx={{
|
||||||
|
p: '4px 4px',
|
||||||
|
m: 1,
|
||||||
|
width: '320px',
|
||||||
|
position: 'relative',
|
||||||
|
}}>
|
||||||
|
<CardMedia
|
||||||
|
sx={{
|
||||||
|
loading: "eager",
|
||||||
|
borderRadius: 2
|
||||||
|
}}
|
||||||
|
onClick={() => handleSelect(row)}
|
||||||
|
component="img"
|
||||||
|
alt={row.name}
|
||||||
|
height="300"
|
||||||
|
|
||||||
|
image={`${row.image}/preview`}
|
||||||
|
/>
|
||||||
|
{renderFavouriteBadge(handleFavourite, row)}
|
||||||
|
{renderRating(handleChangeRating, row)}
|
||||||
|
<CardContent sx={{pb: '4px', pl: 2}}>
|
||||||
|
<Typography variant="h5" minHeight={'50px'} mt={2}>{row.name} </Typography>
|
||||||
|
<List sx={{py: '0px'}}>
|
||||||
|
{row.hasError && (
|
||||||
|
<ListItem sx={{p: '2px 12px 0px 0px', m: '0px'}}>
|
||||||
|
<ListItemText color={'red'}>Имеет ошибку в рецепте или ингредиентах</ListItemText>
|
||||||
|
</ListItem>
|
||||||
|
)}
|
||||||
|
<ListItem sx={{p: '2px 12px 0px 0px', m: '0px'}}>
|
||||||
|
<ListItemText>{"Категория: " + row.category}</ListItemText>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem sx={{p: '2px 12px 0px 0px', m: '0px'}}>
|
||||||
|
<ListItemText>{"Алкоголь: " + row.alcoholic}</ListItemText>
|
||||||
|
</ListItem>
|
||||||
|
{row.volume !== null && (
|
||||||
|
<ListItem sx={{p: '2px 12px 0px 0px', m: '0px'}}>
|
||||||
|
<ListItemText>{"Крепость: ≈" + row.volume}</ListItemText>
|
||||||
|
</ListItem>
|
||||||
|
)}
|
||||||
|
<ListItem sx={{p: '2px 12px 0px 0px', m: '0px'}}>
|
||||||
|
<ListItemText>{"Подача: " + row.glass}</ListItemText>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem sx={{p: '2px 12px 0px 0px', m: '0px'}}>
|
||||||
|
<ListItemText>{"Состав: " + row.components}</ListItemText>
|
||||||
|
</ListItem>
|
||||||
|
{(row.tags && row.tags.length > 0) && (
|
||||||
|
<ListItem sx={{p: '2px 12px 0px 0px', m: '0px'}}>
|
||||||
|
<ListItemText>{"Теги: " + row.tags.replaceAll(',', ', ')}</ListItemText>
|
||||||
|
</ListItem>)}
|
||||||
|
</List>
|
||||||
|
</CardContent>
|
||||||
|
<CardActions>
|
||||||
|
{(row.isAllowed && session.isActive && user.invited) &&
|
||||||
|
<Button variant="contained" onClick={() => pay(row.id)}>Заказать</Button>
|
||||||
|
}
|
||||||
|
{editMenuBlock(row)}
|
||||||
|
</CardActions>
|
||||||
|
</Box>
|
||||||
|
</CocktailItemStyled>
|
||||||
|
</Grid>
|
||||||
|
)
|
||||||
|
}
|
||||||
180
front/src/components/cocktails/CocktailInfoModal.js
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
import DialogTitle from "@mui/material/DialogTitle";
|
||||||
|
import DialogContent from "@mui/material/DialogContent";
|
||||||
|
import Stack from "@mui/material/Stack";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import DialogActions from "@mui/material/DialogActions";
|
||||||
|
import Button from "@mui/material/Button";
|
||||||
|
import Dialog from "@mui/material/Dialog";
|
||||||
|
import * as React from "react";
|
||||||
|
import {useEffect, useState} from "react";
|
||||||
|
import {CardMedia} from "@mui/material";
|
||||||
|
import Paper from "@mui/material/Paper";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import StarBorderIcon from '@mui/icons-material/StarBorder';
|
||||||
|
import IconButton from "@mui/material/IconButton";
|
||||||
|
import ShoppingCartIcon from '@mui/icons-material/ShoppingCart';
|
||||||
|
import DeleteIcon from '@mui/icons-material/Delete';
|
||||||
|
import {IngredientInfoModal} from "../Ingredients/IngredientInfoModal";
|
||||||
|
import {api} from "../../lib/clients/api";
|
||||||
|
import {requests} from "../../requests";
|
||||||
|
import {useAlert} from "../../hooks/useAlert";
|
||||||
|
import {paths} from "../../path";
|
||||||
|
import {Loading} from "../core/Loading";
|
||||||
|
import {useUser} from "../../hooks/useUser";
|
||||||
|
|
||||||
|
export function CocktailInfoModal({open, row, closeHandler}) {
|
||||||
|
const {user} = useUser();
|
||||||
|
const {getError, createError, createSuccess} = useAlert();
|
||||||
|
const [cocktail, setCocktail] = useState(null)
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [selectedIngredient, setSelectedIngredient] = useState(null);
|
||||||
|
const [openIngredientModal, setOpenIngredientModal] = useState(false)
|
||||||
|
const closeIngredientHandler = () => {
|
||||||
|
setOpenIngredientModal(false);
|
||||||
|
setSelectedIngredient(null);
|
||||||
|
}
|
||||||
|
const openIngredientModalHandler = (id) => {
|
||||||
|
api().get(`${requests.bar.ingredient}?id=${id}`)
|
||||||
|
.then((r) => {
|
||||||
|
setSelectedIngredient(r.data)
|
||||||
|
setOpenIngredientModal(true);
|
||||||
|
}).catch(() => createError("Ошибка получения информации об ингредиенте"))
|
||||||
|
}
|
||||||
|
const selectIngredientHandler = (ingredient) => {
|
||||||
|
const url = `${requests.bar.ingredient}?id=${ingredient.id}`;
|
||||||
|
const request = ingredient.isHave ? api().delete(url) : api().put(url);
|
||||||
|
const value = !ingredient.isHave;
|
||||||
|
request.then(() => {
|
||||||
|
const newReceipts = cocktail.receipt.map((r) => {
|
||||||
|
if (r.ingredient.id !== ingredient.id) {
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...r,
|
||||||
|
ingredient: {
|
||||||
|
...ingredient,
|
||||||
|
isHave: value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
setCocktail({
|
||||||
|
...cocktail,
|
||||||
|
receipt: newReceipts
|
||||||
|
})
|
||||||
|
createSuccess("Сохранено")
|
||||||
|
}).catch(() => createError("Ошибка сохранения"))
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLoading(true)
|
||||||
|
if (!row) {
|
||||||
|
setLoading(false)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
api().get(requests.cocktails.modal + row)
|
||||||
|
.then((r) => {
|
||||||
|
setCocktail(r.data)
|
||||||
|
setLoading(false)
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
getError();
|
||||||
|
setLoading(false)
|
||||||
|
closeHandler();
|
||||||
|
})
|
||||||
|
}, [row]);
|
||||||
|
|
||||||
|
if (!row || !cocktail) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
let alko = 0;
|
||||||
|
let volume = 0;
|
||||||
|
return (
|
||||||
|
<Dialog fullWidth={true}
|
||||||
|
open={open} onClose={closeHandler}
|
||||||
|
sx={{
|
||||||
|
'& .MuiDialog-paper': {
|
||||||
|
margin: '8px',
|
||||||
|
},
|
||||||
|
'& .MuiPaper-root': {
|
||||||
|
width: 'calc(100% - 16px)',
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<IngredientInfoModal ingredient={selectedIngredient} open={openIngredientModal}
|
||||||
|
closeHandler={closeIngredientHandler}/>
|
||||||
|
<Loading loading={loading}/>
|
||||||
|
<DialogTitle>
|
||||||
|
<Stack direction='row' justifyContent={'space-between'}>
|
||||||
|
<Box>{cocktail.name}</Box>
|
||||||
|
|
||||||
|
{cocktail.rating.rating > 0 &&
|
||||||
|
(
|
||||||
|
<Stack ml={3} direction='row'>
|
||||||
|
{`${cocktail.rating.rating}/5`}
|
||||||
|
<StarBorderIcon sx={{pb: "2px"}}/>
|
||||||
|
</Stack>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</Stack>
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<CardMedia
|
||||||
|
image={`${cocktail.image}/preview`}
|
||||||
|
sx={{
|
||||||
|
loading: "eager",
|
||||||
|
borderRadius: 2
|
||||||
|
}}
|
||||||
|
component="img"
|
||||||
|
alt={cocktail.name}
|
||||||
|
height="300"
|
||||||
|
/>
|
||||||
|
<Box mt={1}>
|
||||||
|
<Typography>Ингредиенты:</Typography>
|
||||||
|
<Paper sx={{p: 1}} elevation={3}>
|
||||||
|
<Stack>
|
||||||
|
{cocktail.receipt.map((r) => {
|
||||||
|
const hasError = r.count === null || r.unit === null;
|
||||||
|
const measure = hasError ? r.measure : (r.count + " " + r.unit.name)
|
||||||
|
if(alko !== null && volume !== null) {
|
||||||
|
console.log(r)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Stack key={r.ingredient.id} direction='row' justifyContent={'space-between'}
|
||||||
|
mt={1}>
|
||||||
|
<Stack direction='row'>
|
||||||
|
{user.role !== "USER" && (
|
||||||
|
<IconButton size="small" sx={{pb: "2px"}}
|
||||||
|
onClick={() => selectIngredientHandler(r.ingredient)}>
|
||||||
|
{r.ingredient.isHave
|
||||||
|
? (<DeleteIcon fontSize="small"/>)
|
||||||
|
: (<ShoppingCartIcon fontSize="small"/>)
|
||||||
|
}
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
<Typography
|
||||||
|
onClick={() => openIngredientModalHandler(r.ingredient.id)}>{r.ingredient.name}</Typography>
|
||||||
|
</Stack>
|
||||||
|
<Typography color={hasError && 'red'}>{measure}</Typography>
|
||||||
|
</Stack>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Typography mt={2}>Инструкция:</Typography>
|
||||||
|
<Paper sx={{p: 1}} elevation={3}>
|
||||||
|
<Box>
|
||||||
|
{cocktail.instructions}
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
{user.role.includes("ADMIN") && (
|
||||||
|
<Button href={`${paths.bar.cocktailEdit}?id=${cocktail.id}`}>Редактировать</Button>
|
||||||
|
)}
|
||||||
|
<Button onClick={closeHandler}>Закрыть</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
12
front/src/components/cocktails/CocktailItemStyled.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import {styled} from "@mui/material/styles";
|
||||||
|
import {Card} from "@mui/material";
|
||||||
|
|
||||||
|
export const CocktailItemStyled = styled(Card)(({theme}) => ({
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
...theme.typography.body2,
|
||||||
|
padding: theme.spacing(1),
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
...theme.applyStyles('dark', {
|
||||||
|
backgroundColor: '#1A2027',
|
||||||
|
})
|
||||||
|
}));
|
||||||
44
front/src/components/cocktails/CocktailListCard.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import {Card, FormControlLabel} from "@mui/material";
|
||||||
|
import Stack from "@mui/material/Stack";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import IconButton from "@mui/material/IconButton";
|
||||||
|
import InfoRoundedIcon from "@mui/icons-material/InfoRounded";
|
||||||
|
import {paths} from "../../path";
|
||||||
|
import EditIcon from "@mui/icons-material/Edit";
|
||||||
|
import React from "react";
|
||||||
|
import Switch from "@mui/material/Switch";
|
||||||
|
|
||||||
|
export function CocktailListCard({row, changeHandler, infoHandler}) {
|
||||||
|
return (
|
||||||
|
<Card sx={{mb: 1, height: '130px', display: 'relative', pt: 1, borderRadius: '15px'}}>
|
||||||
|
<Stack direction='row' justifyContent='start' alignItems='start'>
|
||||||
|
<Box sx={{width: '100px', height: '100px', ml: 1}}>
|
||||||
|
<img src={row.image} loading='eager' height={'100px'} width={'100px'} alt={row.id}
|
||||||
|
style={{borderRadius: '20%'}}/>
|
||||||
|
</Box>
|
||||||
|
<Stack sx={{width: 'calc(95% - 100px)', pr: 2, ml: 1}}>
|
||||||
|
<Box>{row.name}</Box>
|
||||||
|
<FormControlLabel sx={{mt: 5, pr: 2}}
|
||||||
|
onClick={() => changeHandler(row, !row.inMenu)}
|
||||||
|
value="bottom"
|
||||||
|
control={
|
||||||
|
<Switch color="primary" checked={row.inMenu}/>
|
||||||
|
}
|
||||||
|
label="В меню"
|
||||||
|
labelPlacement="start"
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
<Stack direction='row'>
|
||||||
|
<Stack sx={{width: '5%', mt: 2}} spacing={1} justifyContent='flex-start'>
|
||||||
|
<IconButton size='small' onClick={() => infoHandler(row)}>
|
||||||
|
<InfoRoundedIcon/>
|
||||||
|
</IconButton>
|
||||||
|
<IconButton size='small' href={`${paths.bar.cocktailEdit}?id=${row.id}`}>
|
||||||
|
<EditIcon/>
|
||||||
|
</IconButton>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
76
front/src/components/cocktails/CocktailsList.js
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import {useMemo, useState} from "react";
|
||||||
|
import {getComparator} from "../core/getComparator";
|
||||||
|
import {Accordion, AccordionDetails, AccordionSummary} from "@mui/material";
|
||||||
|
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import {CocktailListCard} from "./CocktailListCard";
|
||||||
|
import {groupByForLoop} from "../core/groupByForLoop";
|
||||||
|
|
||||||
|
export function CocktailsList({rows, grouping, changeHandler, infoHandler}) {
|
||||||
|
const [size, setSize] = useState(20);
|
||||||
|
|
||||||
|
window.addEventListener('scroll', () => {
|
||||||
|
if (window.innerHeight + window.scrollY >= (document.documentElement.scrollHeight - 100)) {
|
||||||
|
if (!grouping) {
|
||||||
|
setSize(size + 10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const visibleRows = useMemo(() => {
|
||||||
|
let res = [];
|
||||||
|
if (rows.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!grouping) {
|
||||||
|
return rows
|
||||||
|
.sort(getComparator("asc", "name"))
|
||||||
|
.slice(0, size)
|
||||||
|
.map((row) => {
|
||||||
|
return (
|
||||||
|
<CocktailListCard row={row} key={row.id}
|
||||||
|
changeHandler={changeHandler} infoHandler={infoHandler}/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const group = groupByForLoop(rows, "category")
|
||||||
|
if (!group || group.size === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Array.from(group.keys())
|
||||||
|
.sort(getComparator())
|
||||||
|
.map((key) => {
|
||||||
|
const list = group.get(key);
|
||||||
|
res.push(
|
||||||
|
<Accordion key={key} sx={{borderRadius: '5px', mb: 1}}>
|
||||||
|
<AccordionSummary
|
||||||
|
expandIcon={<ExpandMoreIcon/>}
|
||||||
|
aria-controls="panel1-content"
|
||||||
|
id="panel1-header"
|
||||||
|
>
|
||||||
|
<Typography component="span">{key}</Typography>
|
||||||
|
</AccordionSummary>
|
||||||
|
<AccordionDetails sx={{p: 1}}>
|
||||||
|
{list.sort(getComparator("asc", "name"))
|
||||||
|
.map((row) => {
|
||||||
|
return (
|
||||||
|
<CocktailListCard row={row} key={row.id}
|
||||||
|
changeHandler={changeHandler} infoHandler={infoHandler}/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</AccordionDetails>
|
||||||
|
</Accordion>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
return res;
|
||||||
|
// eslint-disable-next-line
|
||||||
|
}, [size, rows])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box mt={2}>
|
||||||
|
{visibleRows}
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
167
front/src/components/cocktails/EditCocktailReceipt.js
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import Stack from "@mui/material/Stack";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import IconButton from "@mui/material/IconButton";
|
||||||
|
import AddIcon from "@mui/icons-material/Add";
|
||||||
|
import * as React from "react";
|
||||||
|
import {useEffect, useState} from "react";
|
||||||
|
import {useAlert} from "../../hooks/useAlert";
|
||||||
|
import {api} from "../../lib/clients/api";
|
||||||
|
import {requests} from "../../requests";
|
||||||
|
import {getComparator} from "../core/getComparator";
|
||||||
|
import {Card} from "@mui/material";
|
||||||
|
import {SelectEdit} from "./SelectEdit";
|
||||||
|
import TextField from "@mui/material/TextField";
|
||||||
|
import DeleteForeverIcon from '@mui/icons-material/DeleteForever';
|
||||||
|
|
||||||
|
export function EditCocktailReceipt({receipt, handler}) {
|
||||||
|
const {createError} = useAlert()
|
||||||
|
const [ingredients, setIngredients] = useState([]);
|
||||||
|
const [units, setUnits] = useState([])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
api().get(requests.bar.ingredientList)
|
||||||
|
.then((r) => setIngredients(r.data.sort(getComparator("asc", "name"))))
|
||||||
|
.catch(() => createError("Ошибка получения списка ингредиентов"))
|
||||||
|
|
||||||
|
api().get(requests.bar.unit)
|
||||||
|
.then((r) => setUnits(r.data.sort(getComparator("asc", "name"))))
|
||||||
|
.catch(() => createError("Ошибка получения единиц измерения"))
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const selectHandler = (name, value) => {
|
||||||
|
const ing = ingredients.find((i) => i.name === value)
|
||||||
|
const newState = receipt.map((r, i) => {
|
||||||
|
if (i !== name) {
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id: r.id,
|
||||||
|
ingredient: {
|
||||||
|
id: ing.id,
|
||||||
|
isHave: ing.have,
|
||||||
|
name: ing.name
|
||||||
|
},
|
||||||
|
measure: r.measure
|
||||||
|
}
|
||||||
|
})
|
||||||
|
handler("receipt", newState);
|
||||||
|
checkAllowed(newState);
|
||||||
|
}
|
||||||
|
const unitHandler = (name, value) => {
|
||||||
|
const ing = units.find((i) => i.name === value)
|
||||||
|
const newState = receipt.map((r, i) => {
|
||||||
|
if (i !== name) {
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id: r.id,
|
||||||
|
ingredient: r.ingredient,
|
||||||
|
unit: ing,
|
||||||
|
count: r.count,
|
||||||
|
measure: r.measure
|
||||||
|
}
|
||||||
|
})
|
||||||
|
handler("receipt", newState);
|
||||||
|
checkAllowed(newState);
|
||||||
|
}
|
||||||
|
const removeHandler = (index) => {
|
||||||
|
const arr = receipt.filter((r, i) => i !== index)
|
||||||
|
handler("receipt", arr)
|
||||||
|
checkAllowed(arr)
|
||||||
|
}
|
||||||
|
const addHandler = () => {
|
||||||
|
const oldState = receipt;
|
||||||
|
oldState.push({
|
||||||
|
id: null,
|
||||||
|
ingredient: {
|
||||||
|
id: null,
|
||||||
|
isHave: false,
|
||||||
|
name: ""
|
||||||
|
},
|
||||||
|
measure: ""
|
||||||
|
});
|
||||||
|
handler("receipt", oldState);
|
||||||
|
checkAllowed(oldState);
|
||||||
|
}
|
||||||
|
const checkAllowed = (state) => {
|
||||||
|
handler("isAllowed", !state.map((r) => r.ingredient.isHave).includes(false))
|
||||||
|
}
|
||||||
|
const measureHandler = (index, value) => {
|
||||||
|
const newState = receipt.map((r, i) => {
|
||||||
|
if (index !== i) {
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...r,
|
||||||
|
measure: value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
handler("receipt", newState)
|
||||||
|
}
|
||||||
|
const countHandler = (index, value) => {
|
||||||
|
const newState = receipt.map((r, i) => {
|
||||||
|
if (index !== i) {
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...r,
|
||||||
|
count: value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
handler("receipt", newState)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box mb={2}>
|
||||||
|
{/*Заголовок*/}
|
||||||
|
<Stack direction='row' justifyContent={'space-between'} sx={{mr: 1}}>
|
||||||
|
<Typography ml={1} mt={1}>Рецепт</Typography>
|
||||||
|
<IconButton onClick={() => addHandler()}>
|
||||||
|
<AddIcon/>
|
||||||
|
</IconButton>
|
||||||
|
</Stack>
|
||||||
|
{/*Рецепт*/}
|
||||||
|
<Stack sx={{mr: 1}}>
|
||||||
|
{receipt.map((r, i) => {
|
||||||
|
return (
|
||||||
|
<Card key={i} sx={{ml: 0, mb: 1}}>
|
||||||
|
<Stack>
|
||||||
|
<Stack direction='row'>
|
||||||
|
<SelectEdit width={'calc(65% - 28px)'} array={ingredients} value={r.ingredient.name}
|
||||||
|
handler={selectHandler} label={"Ингредиент"}
|
||||||
|
margin={1} attributeName={i}
|
||||||
|
/>
|
||||||
|
<TextField sx={{width: 'calc(35% - 28px)', mt: 1}}
|
||||||
|
label={"Кол-во"}
|
||||||
|
variant="outlined"
|
||||||
|
disabled
|
||||||
|
value={r.measure}
|
||||||
|
onChange={(e) => measureHandler(i, e.target.value)}
|
||||||
|
/>
|
||||||
|
<IconButton sx={{mt: 2}}
|
||||||
|
onClick={() => removeHandler(i)}
|
||||||
|
>
|
||||||
|
<DeleteForeverIcon/>
|
||||||
|
</IconButton>
|
||||||
|
</Stack>
|
||||||
|
<Stack direction='row' ml={1} mb={1}>
|
||||||
|
<TextField sx={{width: 'calc(35% - 28px)', mt: 1}}
|
||||||
|
label={"Кол-во"}
|
||||||
|
variant="outlined"
|
||||||
|
value={r.count}
|
||||||
|
onChange={(e) => countHandler(i, e.target.value)}
|
||||||
|
/>
|
||||||
|
<SelectEdit width={'calc(65% - 28px)'} array={units} value={!r.unit ? null : r.unit.name}
|
||||||
|
handler={unitHandler} label={"Ед."}
|
||||||
|
margin={1} attributeName={i}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
160
front/src/components/cocktails/FilterBlock.js
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
import {Card, FormControl, FormControlLabel, InputAdornment, InputLabel, OutlinedInput} from "@mui/material";
|
||||||
|
import IconButton from "@mui/material/IconButton";
|
||||||
|
import SearchIcon from "@mui/icons-material/Search";
|
||||||
|
import Tooltip from "@mui/material/Tooltip";
|
||||||
|
import FilterListIcon from "@mui/icons-material/FilterList";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import Grid from "@mui/material/Grid";
|
||||||
|
import Switch from "@mui/material/Switch";
|
||||||
|
import CheckMarks from "./CheckMarks";
|
||||||
|
import Button from "@mui/material/Button";
|
||||||
|
import * as React from "react";
|
||||||
|
import {useEffect, useState} from "react";
|
||||||
|
import {requests} from "../../requests";
|
||||||
|
import {useAlert} from "../../hooks/useAlert";
|
||||||
|
import {api} from "../../lib/clients/api";
|
||||||
|
import {sortList} from "./sortingList";
|
||||||
|
|
||||||
|
const inMenuFilter = [
|
||||||
|
{
|
||||||
|
id: true,
|
||||||
|
name: "Есть в меню"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: false,
|
||||||
|
name: "Нет в меню"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
export function FilterBlock({filter, handleFilterChange, handleClearFilter, barmen, all}) {
|
||||||
|
const {createError} = useAlert();
|
||||||
|
const [glass, setGlass] = useState([]);
|
||||||
|
const [category, setCategory] = useState([]);
|
||||||
|
const [tags, setTags] = useState([])
|
||||||
|
const alcohol = [
|
||||||
|
{
|
||||||
|
name: "Алкогольный",
|
||||||
|
id: "alcohol1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Безалкогольный",
|
||||||
|
id: "alcohol2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Опционально",
|
||||||
|
id: "alcohol3"
|
||||||
|
}];
|
||||||
|
const ingredientCount = [
|
||||||
|
{
|
||||||
|
id: "1IngredientCount",
|
||||||
|
name: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "2IngredientCount",
|
||||||
|
name: 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "3IngredientCount",
|
||||||
|
name: 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "4IngredientCount",
|
||||||
|
name: 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "5IngredientCount",
|
||||||
|
name: 5
|
||||||
|
}]
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
api().get(requests.bar.category)
|
||||||
|
.then((r) => setCategory(r.data))
|
||||||
|
.catch(() => createError("Ошибка получения категорий"))
|
||||||
|
|
||||||
|
api().get(requests.bar.glass)
|
||||||
|
.then((r) => setGlass(r.data))
|
||||||
|
.catch(() => createError("Ошибка получения посуды"))
|
||||||
|
|
||||||
|
api().get(requests.bar.tags)
|
||||||
|
.then((r) => setTags(r.data))
|
||||||
|
.catch(() => createError("Ошибка получения тегов"))
|
||||||
|
// eslint-disable-next-line
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
{/*Строка поиска*/}
|
||||||
|
<FormControl sx={{m: 1, width: '300px'}}>
|
||||||
|
<InputLabel htmlFor="outlined-adornment-amount">Поиск</InputLabel>
|
||||||
|
<OutlinedInput
|
||||||
|
onChange={(e) => handleFilterChange("search", e.target.value)}
|
||||||
|
label="With normal TextField"
|
||||||
|
startAdornment={
|
||||||
|
<InputAdornment position="start">
|
||||||
|
<IconButton edge="end">
|
||||||
|
<SearchIcon/>
|
||||||
|
</IconButton>
|
||||||
|
</InputAdornment>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
{/*Кнопка открытия фильтров*/}
|
||||||
|
<Tooltip title="Filter list">
|
||||||
|
<IconButton onClick={() => handleFilterChange("hidden", !filter.hidden)}>
|
||||||
|
<FilterListIcon/>
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
{/*Блок сортировки*/}
|
||||||
|
<Box hidden={filter.hidden}>
|
||||||
|
<Grid container>
|
||||||
|
{/*Фильтр по алкогольности*/}
|
||||||
|
<CheckMarks rows={sortList} name={"Сортировать по..."} handleChange={handleFilterChange}
|
||||||
|
filterValue={filter.sorting} filterName={"sorting"} nonMulti/>
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
{/*Блок фильтров*/}
|
||||||
|
<Box hidden={filter.hidden}>
|
||||||
|
<Grid container>
|
||||||
|
{/*Фильтр по меню*/}
|
||||||
|
{(barmen && all) && (
|
||||||
|
<CheckMarks rows={inMenuFilter} name={"Есть в меню"} filterName={"inMenu"}
|
||||||
|
filterValue={filter.inMenu}
|
||||||
|
handleChange={handleFilterChange}
|
||||||
|
nonMulti nullValue
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{/*Фильтр по избранным*/}
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Switch inputProps={{'aria-label': 'controlled'}}
|
||||||
|
onChange={() => handleFilterChange("onlyFavourite", !filter.onlyFavourite)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="Только избранные"
|
||||||
|
sx={{ml: 1}}
|
||||||
|
/>
|
||||||
|
{/*Фильтр по алкогольности*/}
|
||||||
|
<CheckMarks rows={alcohol} name={"Алкогольность"} handleChange={handleFilterChange}
|
||||||
|
filterValue={filter.alcohol} filterName={"alcohol"}/>
|
||||||
|
{/*Фильтр по категории*/}
|
||||||
|
{category.length > 0 && (
|
||||||
|
<CheckMarks rows={category} name={"Категории"} filterValue={filter.category}
|
||||||
|
filterName={"category"} handleChange={handleFilterChange}/>)}
|
||||||
|
{/*Фильтр по посуде*/}
|
||||||
|
{glass.length > 0 && (<CheckMarks rows={glass} name={"Подача"} handleChange={handleFilterChange}
|
||||||
|
filterValue={filter.glass} filterName={"glass"}/>)}
|
||||||
|
{/*Фильтр по тегам*/}
|
||||||
|
{tags.length > 0 && (<CheckMarks rows={tags} name={"Теги"} handleChange={handleFilterChange}
|
||||||
|
filterValue={filter.tags} filterName={"tags"}/>)}
|
||||||
|
{/*Фильтр по нехватке ингредиентов*/}
|
||||||
|
{/*todo: доделать эти фильтры в беке*/}
|
||||||
|
{/*{(barmen && all) && (<CheckMarks rows={ingredientCount} name={"Не хватает ингредиентов"}*/}
|
||||||
|
{/* handleChange={handleFilterChange}*/}
|
||||||
|
{/* nonMulti nullValue*/}
|
||||||
|
{/* filterValue={filter.iCount} filterName={"iCount"}/>)}*/}
|
||||||
|
<Button onClick={() => handleClearFilter()}>Сбросить</Button>
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
17
front/src/components/cocktails/NoResult.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import Grid from "@mui/material/Grid";
|
||||||
|
import {Stack} from "@mui/material";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import {CocktailItemStyled} from "./CocktailItemStyled";
|
||||||
|
|
||||||
|
export function NoResult({load}) {
|
||||||
|
return (
|
||||||
|
<Grid item lg={4} md={6} sm={12} xl={3} hidden={!load}>
|
||||||
|
<CocktailItemStyled>
|
||||||
|
<Stack align="center" sx={{width: "350px"}}>
|
||||||
|
<Typography variant="h5" minHeight={'50px'} mt={2}>Нет результатов</Typography>
|
||||||
|
<Typography>Попробуйте заглянуть позднее</Typography>
|
||||||
|
</Stack>
|
||||||
|
</CocktailItemStyled>
|
||||||
|
</Grid>
|
||||||
|
)
|
||||||
|
}
|
||||||
25
front/src/components/cocktails/SelectEdit.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import {FormControl, InputLabel} from "@mui/material";
|
||||||
|
import Select from "@mui/material/Select";
|
||||||
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
export function SelectEdit({label, value, array, handler, attributeName, width, margin}) {
|
||||||
|
return (
|
||||||
|
<FormControl sx={{width: width, m: margin}}>
|
||||||
|
<InputLabel>{label}</InputLabel>
|
||||||
|
<Select
|
||||||
|
autoWidth
|
||||||
|
label={label}
|
||||||
|
value={!value ? "" : value}
|
||||||
|
onChange={(e) => handler(attributeName, e.target.value)}
|
||||||
|
>
|
||||||
|
<MenuItem value="">
|
||||||
|
<em>None</em>
|
||||||
|
</MenuItem>
|
||||||
|
{array.map((c) => {
|
||||||
|
return (<MenuItem key={c.id} value={c.name}>{c.name}</MenuItem>)
|
||||||
|
})}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)
|
||||||
|
}
|
||||||
27
front/src/components/cocktails/sortingList.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
export const sortList = [
|
||||||
|
{
|
||||||
|
id: "name|asc",
|
||||||
|
name: "Название по возрастанию"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "name|desc",
|
||||||
|
name: "Название по убыванию"
|
||||||
|
},
|
||||||
|
// todo: добавить сортировки в беке
|
||||||
|
// {
|
||||||
|
// id: "rating.rating|desc",
|
||||||
|
// name: "Сначала с оценкой"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: "rating.rating|asc",
|
||||||
|
// name: "Сначала без оценки"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: "rating.favourite|desc",
|
||||||
|
// name: "Сначала избранные"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: "rating.favourite|asc",
|
||||||
|
// name: "Сначала не избранные"
|
||||||
|
// }
|
||||||
|
]
|
||||||
12
front/src/components/core/Loading.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import {Backdrop, CircularProgress} from "@mui/material";
|
||||||
|
|
||||||
|
export function Loading({loading}) {
|
||||||
|
return (
|
||||||
|
<Backdrop
|
||||||
|
sx={{color: '#fff', zIndex: (theme) => theme.zIndex.drawer + 1}}
|
||||||
|
open={loading}
|
||||||
|
>
|
||||||
|
<CircularProgress color="inherit"/>
|
||||||
|
</Backdrop>
|
||||||
|
);
|
||||||
|
}
|
||||||
10
front/src/components/core/LocalizationProvider.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import {AdapterDayjs} from '@mui/x-date-pickers/AdapterDayjs';
|
||||||
|
import {LocalizationProvider as Provider} from '@mui/x-date-pickers/LocalizationProvider';
|
||||||
|
import * as React from 'react';
|
||||||
|
import 'dayjs/locale/ru'
|
||||||
|
|
||||||
|
export function LocalizationProvider({children}) {
|
||||||
|
return (
|
||||||
|
<Provider dateAdapter={AdapterDayjs} adapterLocale="ru-Ru">{children}</Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
38
front/src/components/core/Logo.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import { useColorScheme } from '@mui/material/styles';
|
||||||
|
import {NoSsr} from "./NoSsr";
|
||||||
|
|
||||||
|
const HEIGHT = 60;
|
||||||
|
const WIDTH = 60;
|
||||||
|
|
||||||
|
export function Logo({ color = 'dark', emblem, height = HEIGHT, width = WIDTH }) {
|
||||||
|
let url;
|
||||||
|
|
||||||
|
if (emblem) {
|
||||||
|
url = color === 'light' ? '/assets/logo-emblem.svg' : '/assets/logo-emblem--dark.svg';
|
||||||
|
} else {
|
||||||
|
url = color === 'light' ? '/assets/logo.svg' : '/assets/logo--dark.svg';
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Box alt="logo" component="img" height={height} src={url} width={width} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DynamicLogo({
|
||||||
|
colorDark = 'light',
|
||||||
|
colorLight = 'dark',
|
||||||
|
height = HEIGHT,
|
||||||
|
width = WIDTH,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
const { colorScheme } = useColorScheme();
|
||||||
|
const color = colorScheme === 'dark' ? colorDark : colorLight;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NoSsr fallback={<Box sx={{ height: `${height}px`, width: `${width}px` }} />}>
|
||||||
|
<Logo color={color} height={height} width={width} {...props} />
|
||||||
|
</NoSsr>
|
||||||
|
);
|
||||||
|
}
|
||||||
9
front/src/components/core/ModalDialogStyled.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import {styled} from "@mui/material/styles";
|
||||||
|
import Dialog from "@mui/material/Dialog";
|
||||||
|
|
||||||
|
export const ModalDialogStyled = styled(Dialog)(({theme}) => ({
|
||||||
|
backdrop: {
|
||||||
|
margin: '4px',
|
||||||
|
border: 'solid',
|
||||||
|
},
|
||||||
|
}));
|
||||||
25
front/src/components/core/NoSsr.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import useEnhancedEffect from '@mui/utils/useEnhancedEffect';
|
||||||
|
|
||||||
|
export function NoSsr(props) {
|
||||||
|
const {children, defer = false, fallback = null} = props;
|
||||||
|
const [mountedState, setMountedState] = React.useState(false);
|
||||||
|
|
||||||
|
useEnhancedEffect(() => {
|
||||||
|
if (!defer) {
|
||||||
|
setMountedState(true);
|
||||||
|
}
|
||||||
|
}, [defer]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (defer) {
|
||||||
|
setMountedState(true);
|
||||||
|
}
|
||||||
|
}, [defer]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{mountedState ? children : fallback}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
24
front/src/components/core/TabPanel.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import PropTypes from "prop-types";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
export function CustomTabPanel(props) {
|
||||||
|
const {children, value, index, ...other} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="tabpanel"
|
||||||
|
hidden={value !== index}
|
||||||
|
id={`simple-tabpanel-${index}`}
|
||||||
|
aria-labelledby={`simple-tab-${index}`}
|
||||||
|
{...other}
|
||||||
|
>
|
||||||
|
{value === index && children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomTabPanel.propTypes = {
|
||||||
|
children: PropTypes.node,
|
||||||
|
index: PropTypes.number.isRequired,
|
||||||
|
value: PropTypes.number.isRequired,
|
||||||
|
};
|
||||||
81
front/src/components/core/ThemeSwitch.js
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import {styled, useColorScheme} from "@mui/material/styles";
|
||||||
|
import Switch from "@mui/material/Switch";
|
||||||
|
import React from "react";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
|
||||||
|
export function ThemeSwitch() {
|
||||||
|
const {mode, setMode} = useColorScheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
alignItems: 'center',
|
||||||
|
// backgroundColor: 'var(--mui-palette-neutral-950)',
|
||||||
|
border: '1px solid var(--mui-palette-neutral-700)',
|
||||||
|
borderRadius: '12px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
display: 'flex',
|
||||||
|
p: '4px 12px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<StyledSwitch
|
||||||
|
checked={mode === 'dark'}
|
||||||
|
onChange={(e) => setMode(e.target.checked ? 'dark' : 'light')}
|
||||||
|
inputProps={{'aria-label': 'controlled'}}
|
||||||
|
/>
|
||||||
|
<Box sx={{flex: '1 1 auto'}}>
|
||||||
|
<Typography color="inherit" variant="subtitle1">
|
||||||
|
{(mode === 'dark' ? "Темная " : "Светлая ") + "тема"}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const StyledSwitch = styled(Switch)(({theme}) => ({
|
||||||
|
width: 62,
|
||||||
|
height: 34,
|
||||||
|
padding: 7,
|
||||||
|
'& .MuiSwitch-switchBase': {
|
||||||
|
margin: 1,
|
||||||
|
padding: 0,
|
||||||
|
transform: 'translateX(6px)',
|
||||||
|
'&.Mui-checked': {
|
||||||
|
color: '#fff',
|
||||||
|
transform: 'translateX(22px)',
|
||||||
|
'& .MuiSwitch-thumb:before': {
|
||||||
|
backgroundImage: `url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 20 20"><path fill="${encodeURIComponent(
|
||||||
|
'#fff',
|
||||||
|
)}" d="M4.2 2.5l-.7 1.8-1.8.7 1.8.7.7 1.8.6-1.8L6.7 5l-1.9-.7-.6-1.8zm15 8.3a6.7 6.7 0 11-6.6-6.6 5.8 5.8 0 006.6 6.6z"/></svg>')`,
|
||||||
|
},
|
||||||
|
'& + .MuiSwitch-track': {
|
||||||
|
opacity: 1,
|
||||||
|
backgroundColor: theme.palette.mode === 'dark' ? '#8796A5' : '#aab4be',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'& .MuiSwitch-thumb': {
|
||||||
|
backgroundColor: theme.palette.mode === 'dark' ? '#003892' : '#001e3c',
|
||||||
|
width: 32,
|
||||||
|
height: 32,
|
||||||
|
'&::before': {
|
||||||
|
content: "''",
|
||||||
|
position: 'absolute',
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
backgroundRepeat: 'no-repeat',
|
||||||
|
backgroundPosition: 'center',
|
||||||
|
backgroundImage: `url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 20 20"><path fill="${encodeURIComponent(
|
||||||
|
'#fff',
|
||||||
|
)}" d="M9.305 1.667V3.75h1.389V1.667h-1.39zm-4.707 1.95l-.982.982L5.09 6.072l.982-.982-1.473-1.473zm10.802 0L13.927 5.09l.982.982 1.473-1.473-.982-.982zM10 5.139a4.872 4.872 0 00-4.862 4.86A4.872 4.872 0 0010 14.862 4.872 4.872 0 0014.86 10 4.872 4.872 0 0010 5.139zm0 1.389A3.462 3.462 0 0113.471 10a3.462 3.462 0 01-3.473 3.472A3.462 3.462 0 016.527 10 3.462 3.462 0 0110 6.528zM1.665 9.305v1.39h2.083v-1.39H1.666zm14.583 0v1.39h2.084v-1.39h-2.084zM5.09 13.928L3.616 15.4l.982.982 1.473-1.473-.982-.982zm9.82 0l-.982.982 1.473 1.473.982-.982-1.473-1.473zM9.305 16.25v2.083h1.389V16.25h-1.39z"/></svg>')`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'& .MuiSwitch-track': {
|
||||||
|
opacity: 1,
|
||||||
|
backgroundColor: theme.palette.mode === 'dark' ? '#8796A5' : '#aab4be',
|
||||||
|
borderRadius: 20 / 2,
|
||||||
|
},
|
||||||
|
}));
|
||||||
92
front/src/components/core/UserPopover.js
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Divider from '@mui/material/Divider';
|
||||||
|
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||||
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
|
import MenuList from '@mui/material/MenuList';
|
||||||
|
import Popover from '@mui/material/Popover';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import {SignOut as SignOutIcon} from '@phosphor-icons/react/dist/ssr/SignOut';
|
||||||
|
import {logger} from "../../lib/DefaultLogger";
|
||||||
|
import {useAuth} from "../../hooks/useAuth";
|
||||||
|
import {authClient} from "../../lib/clients/AuthClient";
|
||||||
|
import {useLocation} from "react-router-dom";
|
||||||
|
import {useUser} from "../../hooks/useUser";
|
||||||
|
|
||||||
|
export function UserPopover({anchorEl, onClose, open}) {
|
||||||
|
const {checkSession} = useAuth();
|
||||||
|
const {user, session} = useUser();
|
||||||
|
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
const handleSignOut = React.useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const {error} = await authClient.signOut();
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
logger.error('Sign out error', error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh the auth state
|
||||||
|
await checkSession?.();
|
||||||
|
|
||||||
|
// UserProvider, for this case, will not refresh the router and we need to do it manually
|
||||||
|
window.location.reload();
|
||||||
|
// After refresh, AuthGuard will handle the redirect
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('Sign out error', err);
|
||||||
|
}
|
||||||
|
}, [checkSession, location]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
anchorOrigin={{horizontal: 'left', vertical: 'bottom'}}
|
||||||
|
onClose={onClose}
|
||||||
|
open={open}
|
||||||
|
slotProps={{paper: {sx: {width: '240px'}}}}
|
||||||
|
>
|
||||||
|
<Box sx={{p: '16px 20px '}}>
|
||||||
|
{userDescriptor(user, session)}
|
||||||
|
</Box>
|
||||||
|
<Divider/>
|
||||||
|
<MenuList disablePadding sx={{p: '8px', '& .MuiMenuItem-root': {borderRadius: 1}}}>
|
||||||
|
{/*<MenuItem component={'a'} href={paths.dashboard.settings} onClick={onClose}>*/}
|
||||||
|
{/* <ListItemIcon>*/}
|
||||||
|
{/* <GearSixIcon fontSize="var(--icon-fontSize-md)"/>*/}
|
||||||
|
{/* </ListItemIcon>*/}
|
||||||
|
{/* Настройки*/}
|
||||||
|
{/*</MenuItem>*/}
|
||||||
|
{/*<MenuItem component={'a'} href={paths.dashboard.account} onClick={onClose}>*/}
|
||||||
|
{/* <ListItemIcon>*/}
|
||||||
|
{/* <UserIcon fontSize="var(--icon-fontSize-md)"/>*/}
|
||||||
|
{/* </ListItemIcon>*/}
|
||||||
|
{/* Профиль*/}
|
||||||
|
{/*</MenuItem>*/}
|
||||||
|
<MenuItem onClick={handleSignOut}>
|
||||||
|
<ListItemIcon>
|
||||||
|
<SignOutIcon fontSize="var(--icon-fontSize-md)"/>
|
||||||
|
</ListItemIcon>
|
||||||
|
Выход
|
||||||
|
</MenuItem>
|
||||||
|
</MenuList>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function userDescriptor(user, session) {
|
||||||
|
if (!user) {
|
||||||
|
return (<Typography variant="subtitle1">Ошибка загрузки данных</Typography>);
|
||||||
|
}
|
||||||
|
|
||||||
|
const open = (session.isActive && user.invited) ? "открыт" : "закрыт";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Typography variant="subtitle1">{user.name + " " + user.lastName}</Typography>
|
||||||
|
<Typography color="text.secondary" variant="body2">{user.id}</Typography>
|
||||||
|
<Typography color="text.secondary" variant="body2">{`Бар ${open}`}</Typography>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
29
front/src/components/core/descendingComparator.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
export function descendingComparator(a, b, orderBy, lastOrder) {
|
||||||
|
if (getValue(b, orderBy) < getValue(a, orderBy)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (getValue(b, orderBy) > getValue(a, orderBy)) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (lastOrder && orderBy !== lastOrder) {
|
||||||
|
if (getValue(b, lastOrder) < getValue(a, lastOrder)) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (getValue(b, lastOrder) > getValue(a, lastOrder)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getValue(obj, orderBy) {
|
||||||
|
if (!orderBy) {
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
const split = orderBy.split(".")
|
||||||
|
let res = obj[split[0]];
|
||||||
|
for (let i = 1; i < split.length; i++) {
|
||||||
|
res = res[split[i]];
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
10
front/src/components/core/getComparator.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import {descendingComparator} from "./descendingComparator";
|
||||||
|
|
||||||
|
export function getComparator(order, orderBy, lastOrder) {
|
||||||
|
if(!order) {
|
||||||
|
order = "asc"
|
||||||
|
}
|
||||||
|
return order === 'desc'
|
||||||
|
? (a, b) => descendingComparator(a, b, orderBy, lastOrder)
|
||||||
|
: (a, b) => -descendingComparator(a, b, orderBy, lastOrder);
|
||||||
|
}
|
||||||
14
front/src/components/core/groupByForLoop.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
export const groupByForLoop = (arr, prop) => {
|
||||||
|
const grouped = new Map();
|
||||||
|
for (let i = 0; i < arr.length; i++) {
|
||||||
|
let key = arr[i][prop];
|
||||||
|
if (!key) {
|
||||||
|
key = "Без категории"
|
||||||
|
}
|
||||||
|
if (!grouped.has(key)) {
|
||||||
|
grouped.set(key, []);
|
||||||
|
}
|
||||||
|
grouped.get(key).push(arr[i]);
|
||||||
|
}
|
||||||
|
return grouped;
|
||||||
|
};
|
||||||
37
front/src/components/core/navIcons.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import {ChartPie as ChartPieIcon} from '@phosphor-icons/react/dist/ssr/ChartPie';
|
||||||
|
import {GearSix as GearSixIcon} from '@phosphor-icons/react/dist/ssr/GearSix';
|
||||||
|
import {PlugsConnected as PlugsConnectedIcon} from '@phosphor-icons/react/dist/ssr/PlugsConnected';
|
||||||
|
import {User as UserIcon} from '@phosphor-icons/react/dist/ssr/User';
|
||||||
|
import {Users as UsersIcon} from '@phosphor-icons/react/dist/ssr/Users';
|
||||||
|
import {XSquare} from '@phosphor-icons/react/dist/ssr/XSquare';
|
||||||
|
import {
|
||||||
|
Basket,
|
||||||
|
BookOpen,
|
||||||
|
Books,
|
||||||
|
Cheers,
|
||||||
|
CoffeeBean,
|
||||||
|
Coins,
|
||||||
|
Martini,
|
||||||
|
Storefront,
|
||||||
|
Users,
|
||||||
|
Wallet
|
||||||
|
} from "@phosphor-icons/react";
|
||||||
|
|
||||||
|
export const navIcons = {
|
||||||
|
'menu': BookOpen,
|
||||||
|
'list': Books,
|
||||||
|
'storefront': Storefront,
|
||||||
|
'wallet': Wallet,
|
||||||
|
'cocktail': Martini,
|
||||||
|
'visitors': Users,
|
||||||
|
'orders': Cheers,
|
||||||
|
'basket': Basket,
|
||||||
|
'coins': Coins,
|
||||||
|
'ingredients': CoffeeBean,
|
||||||
|
'chart-pie': ChartPieIcon,
|
||||||
|
'gear-six': GearSixIcon,
|
||||||
|
'plugs-connected': PlugsConnectedIcon,
|
||||||
|
'x-square': XSquare,
|
||||||
|
user: UserIcon,
|
||||||
|
users: UsersIcon,
|
||||||
|
}
|
||||||
6
front/src/components/core/tabProps.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export function a11yProps(index) {
|
||||||
|
return {
|
||||||
|
id: `simple-tab-${index}`,
|
||||||
|
'aria-controls': `simple-tabpanel-${index}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
65
front/src/components/navigation/MainNav.js
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import Avatar from '@mui/material/Avatar';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import IconButton from '@mui/material/IconButton';
|
||||||
|
import Stack from '@mui/material/Stack';
|
||||||
|
import {List as ListIcon} from '@phosphor-icons/react/dist/ssr/List';
|
||||||
|
// import NotificationsIcon from '@mui/icons-material/Notifications';
|
||||||
|
|
||||||
|
import {usePopover} from "../../hooks/usePopover";
|
||||||
|
import {MobileNav} from "./MobileNav";
|
||||||
|
import {UserPopover} from "../core/UserPopover";
|
||||||
|
// import Tooltip from "@mui/material/Tooltip";
|
||||||
|
// import {Badge} from "@mui/material";
|
||||||
|
// import {useAlert} from "../../hooks/useAlert";
|
||||||
|
|
||||||
|
export function MainNav() {
|
||||||
|
const [openNav, setOpenNav] = React.useState(false);
|
||||||
|
// const {notImplement} = useAlert();
|
||||||
|
|
||||||
|
const userPopover = usePopover();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Box
|
||||||
|
component="header"
|
||||||
|
sx={{
|
||||||
|
borderBottom: '1px solid var(--mui-palette-divider)',
|
||||||
|
backgroundColor: 'var(--mui-palette-background-paper)',
|
||||||
|
position: 'sticky',
|
||||||
|
top: 0,
|
||||||
|
zIndex: 'var(--mui-zIndex-appBar)',
|
||||||
|
height: '64px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack direction="row" spacing={3}
|
||||||
|
sx={{alignItems: 'center', justifyContent: 'space-between', minHeight: '64px', px: 2}}>
|
||||||
|
<Stack sx={{alignItems: 'center'}} direction="row" spacing={3}>
|
||||||
|
<IconButton onClick={() => setOpenNav(true)} sx={{display: {xl: 'none'}}}>
|
||||||
|
<ListIcon/>
|
||||||
|
</IconButton>
|
||||||
|
</Stack>
|
||||||
|
<Stack sx={{alignItems: 'center'}} direction="row" spacing={2}>
|
||||||
|
{/*<Tooltip title="Уведомления" onClick={() => notImplement()}>*/}
|
||||||
|
{/* <Badge badgeContent={10} color="success" variant="standart">*/}
|
||||||
|
{/* <IconButton>*/}
|
||||||
|
{/* <NotificationsIcon/>*/}
|
||||||
|
{/* </IconButton>*/}
|
||||||
|
{/* </Badge>*/}
|
||||||
|
{/*</Tooltip>*/}
|
||||||
|
<Avatar onClick={userPopover.handleOpen} ref={userPopover.anchorRef} src="/assets/avatar.png"
|
||||||
|
sx={{cursor: 'pointer'}}/>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
<UserPopover anchorEl={userPopover.anchorRef.current} onClose={userPopover.handleClose}
|
||||||
|
open={userPopover.open}/>
|
||||||
|
<MobileNav
|
||||||
|
onClose={() => {
|
||||||
|
setOpenNav(false);
|
||||||
|
}}
|
||||||
|
open={openNav}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
38
front/src/components/navigation/MobileNav.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import Drawer from '@mui/material/Drawer';
|
||||||
|
import {NavigationMenu} from "./NavigationMenu";
|
||||||
|
|
||||||
|
//Боковое меню
|
||||||
|
export function MobileNav({open, onClose}) {
|
||||||
|
return (
|
||||||
|
<Drawer
|
||||||
|
PaperProps={{
|
||||||
|
sx: {
|
||||||
|
'--MobileNav-background': 'var(--mui-palette-neutral-950)',
|
||||||
|
'--MobileNav-color': 'var(--mui-palette-common-white)',
|
||||||
|
'--NavItem-color': 'var(--mui-palette-neutral-300)',
|
||||||
|
'--NavItem-hover-background': 'rgba(255, 255, 255, 0.04)',
|
||||||
|
'--NavItem-active-background': 'var(--mui-palette-primary-main)',
|
||||||
|
'--NavItem-active-color': 'var(--mui-palette-primary-contrastText)',
|
||||||
|
'--NavItem-disabled-color': 'var(--mui-palette-neutral-500)',
|
||||||
|
'--NavItem-icon-color': 'var(--mui-palette-neutral-400)',
|
||||||
|
'--NavItem-icon-active-color': 'var(--mui-palette-primary-contrastText)',
|
||||||
|
'--NavItem-icon-disabled-color': 'var(--mui-palette-neutral-600)',
|
||||||
|
bgcolor: 'var(--MobileNav-background)',
|
||||||
|
color: 'var(--MobileNav-color)',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
maxWidth: '100%',
|
||||||
|
scrollbarWidth: 'none',
|
||||||
|
width: 'var(--MobileNav-width)',
|
||||||
|
zIndex: 'var(--MobileNav-zIndex)',
|
||||||
|
'&::-webkit-scrollbar': {display: 'none'},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
onClose={onClose}
|
||||||
|
open={open}
|
||||||
|
>
|
||||||
|
<NavigationMenu/>
|
||||||
|
</Drawer>
|
||||||
|
);
|
||||||
|
}
|
||||||
74
front/src/components/navigation/NavItem.js
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import Stack from "@mui/material/Stack";
|
||||||
|
import {isNavItemActive} from "../../lib/isNavItemActive";
|
||||||
|
import {navIcons} from "../core/navIcons";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import {Link} from "react-router-dom";
|
||||||
|
|
||||||
|
export function renderNavItems({items = [], pathname}) {
|
||||||
|
const children = items.reduce((acc, curr) => {
|
||||||
|
const {key, ...item} = curr;
|
||||||
|
acc.push(<NavItem key={key} pathname={pathname} {...item} />);
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack key={"stack-NavItem-key"} component="ul" spacing={1} sx={{listStyle: 'none', m: 0, p: 0}}>
|
||||||
|
{children}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function NavItem({disabled, external, href, icon, matcher, pathname, title}) {
|
||||||
|
const active = isNavItemActive({disabled, external, href, matcher, pathname});
|
||||||
|
const Icon = icon ? navIcons[icon] : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li>
|
||||||
|
<Link to={href} style={{ textDecoration: 'none' }}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
alignItems: 'center',
|
||||||
|
borderRadius: 1,
|
||||||
|
color: 'var(--NavItem-color)',
|
||||||
|
cursor: 'pointer',
|
||||||
|
display: 'flex',
|
||||||
|
flex: '0 0 auto',
|
||||||
|
gap: 1,
|
||||||
|
p: '6px 16px',
|
||||||
|
position: 'relative',
|
||||||
|
textDecoration: 'none',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
...(disabled && {
|
||||||
|
bgcolor: 'var(--NavItem-disabled-background)',
|
||||||
|
color: 'var(--NavItem-disabled-color)',
|
||||||
|
cursor: 'not-allowed',
|
||||||
|
}),
|
||||||
|
...(active && {
|
||||||
|
bgcolor: 'var(--NavItem-active-background)',
|
||||||
|
color: 'var(--NavItem-active-color)'
|
||||||
|
}),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{alignItems: 'center', display: 'flex', justifyContent: 'center', flex: '0 0 auto'}}>
|
||||||
|
{Icon ? (
|
||||||
|
<Icon
|
||||||
|
fill={active ? 'var(--NavItem-icon-active-color)' : 'var(--NavItem-icon-color)'}
|
||||||
|
fontSize="var(--icon-fontSize-md)"
|
||||||
|
weight={active ? 'fill' : undefined}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</Box>
|
||||||
|
<Box sx={{flex: '1 1 auto'}}>
|
||||||
|
<Typography
|
||||||
|
component="span"
|
||||||
|
sx={{color: 'inherit', fontSize: '0.875rem', fontWeight: 500, lineHeight: '28px'}}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
56
front/src/components/navigation/NavigationMenu.js
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import Stack from "@mui/material/Stack";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import {ThemeSwitch} from "../core/ThemeSwitch";
|
||||||
|
import Divider from "@mui/material/Divider";
|
||||||
|
import {renderNavItems} from "./NavItem";
|
||||||
|
import {navItems} from "../../navItems";
|
||||||
|
import React, {useEffect, useState} from "react";
|
||||||
|
import {useLocation} from "react-router-dom";
|
||||||
|
import {useUser} from "../../hooks/useUser";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
|
||||||
|
function renderSpecialItems(items, label, pathname) {
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<hr/>
|
||||||
|
<Typography pl={"20px"} pb={1} variant="subtitle2" color="textSecondary">{label}</Typography>
|
||||||
|
{renderNavItems({items: items, pathname: pathname})}
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NavigationMenu() {
|
||||||
|
const location = useLocation();
|
||||||
|
const pathname = location.pathname;
|
||||||
|
const {user} = useUser();
|
||||||
|
const [items, setItems] = useState(null)
|
||||||
|
|
||||||
|
const userChild = navItems.filter((item) => !item.forBarmen && !item.forAdmin)
|
||||||
|
const barmenChild = navItems.filter((item) => item.forBarmen)
|
||||||
|
const adminChild = navItems.filter((item) => item.forAdmin)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const role = !user ? "USER" : Object.keys(user).length === 0 ? "USER" : user.role
|
||||||
|
const newState = (
|
||||||
|
<Box component="nav" sx={{flex: '1 1 auto', p: '12px'}}>
|
||||||
|
{renderNavItems({items: userChild, pathname: pathname})}
|
||||||
|
{role !== "USER" && renderSpecialItems(barmenChild, "Для бармена:", pathname)}
|
||||||
|
{role === "ADMIN" && renderSpecialItems(adminChild, "Для админа", pathname)}
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
setItems(newState)
|
||||||
|
// eslint-disable-next-line
|
||||||
|
}, [user, pathname]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/*верхняя стопка*/}
|
||||||
|
<Stack spacing={2} sx={{p: 2, height: '63px'}}>
|
||||||
|
<ThemeSwitch/>
|
||||||
|
</Stack>
|
||||||
|
<Divider sx={{borderColor: 'var(--mui-palette-neutral-700)'}}/>
|
||||||
|
{/*меню навигации*/}
|
||||||
|
{items}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
37
front/src/components/navigation/SideNav.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import {NavigationMenu} from "./NavigationMenu";
|
||||||
|
|
||||||
|
export function SideNav() {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
'--SideNav-background': 'var(--mui-palette-neutral-950)',
|
||||||
|
'--SideNav-color': 'var(--mui-palette-common-white)',
|
||||||
|
'--NavItem-color': 'var(--mui-palette-neutral-300)',
|
||||||
|
'--NavItem-hover-background': 'rgba(255, 255, 255, 0.04)',
|
||||||
|
'--NavItem-active-background': 'var(--mui-palette-primary-main)',
|
||||||
|
'--NavItem-active-color': 'var(--mui-palette-primary-contrastText)',
|
||||||
|
'--NavItem-disabled-color': 'var(--mui-palette-neutral-500)',
|
||||||
|
'--NavItem-icon-color': 'var(--mui-palette-neutral-400)',
|
||||||
|
'--NavItem-icon-active-color': 'var(--mui-palette-primary-contrastText)',
|
||||||
|
'--NavItem-icon-disabled-color': 'var(--mui-palette-neutral-600)',
|
||||||
|
bgcolor: 'var(--SideNav-background)',
|
||||||
|
color: 'var(--SideNav-color)',
|
||||||
|
display: {xs: 'none', xl: 'flex'},
|
||||||
|
flexDirection: 'column',
|
||||||
|
height: '100%',
|
||||||
|
left: 0,
|
||||||
|
maxWidth: '100%',
|
||||||
|
position: 'fixed',
|
||||||
|
scrollbarWidth: 'none',
|
||||||
|
top: 0,
|
||||||
|
width: 'var(--SideNav-width)',
|
||||||
|
zIndex: 'var(--SideNav-zIndex)',
|
||||||
|
'&::-webkit-scrollbar': {display: 'none'},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<NavigationMenu/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
113
front/src/components/orders/EnhancedTable.js
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import Paper from "@mui/material/Paper";
|
||||||
|
import TableContainer from "@mui/material/TableContainer";
|
||||||
|
import Table from "@mui/material/Table";
|
||||||
|
import TableBody from "@mui/material/TableBody";
|
||||||
|
import TableRow from "@mui/material/TableRow";
|
||||||
|
import TableCell from "@mui/material/TableCell";
|
||||||
|
import TablePagination from "@mui/material/TablePagination";
|
||||||
|
import {getComparator} from "../core/getComparator";
|
||||||
|
import {EnhancedTableToolbar} from "./EnhancedTableToolbar";
|
||||||
|
import {EnhancedTableHead} from "./EnhancedTableHead";
|
||||||
|
|
||||||
|
export default function EnhancedTable({name, rows, cells, handleSelect, filterField, filterEqual, filterValue}) {
|
||||||
|
//сортировка убывание/возрастание
|
||||||
|
const [order, setOrder] = React.useState('desc');
|
||||||
|
//По какому полю сортируем
|
||||||
|
const [orderBy, setOrderBy] = React.useState('id');
|
||||||
|
//выбранная страница
|
||||||
|
const [page, setPage] = React.useState(0);
|
||||||
|
//количество элементов на странице
|
||||||
|
const [rowsPerPage, setRowsPerPage] = React.useState(10);
|
||||||
|
|
||||||
|
const handleRequestSort = (event, property) => {
|
||||||
|
const isAsc = orderBy === property && order === 'asc';
|
||||||
|
setOrder(isAsc ? 'desc' : 'asc');
|
||||||
|
setOrderBy(property);
|
||||||
|
};
|
||||||
|
const handleChangePage = (event, newPage) => {
|
||||||
|
setPage(newPage);
|
||||||
|
};
|
||||||
|
const handleChangeRowsPerPage = (event) => {
|
||||||
|
setRowsPerPage(parseInt(event.target.value, 10));
|
||||||
|
setPage(0);
|
||||||
|
};
|
||||||
|
const getTableValue = (obj, index) => {
|
||||||
|
let indexArr = index.split(".");
|
||||||
|
let object = obj;
|
||||||
|
for (let i of indexArr) {
|
||||||
|
object = object[i];
|
||||||
|
}
|
||||||
|
return object;
|
||||||
|
}
|
||||||
|
|
||||||
|
const visibleRows = React.useMemo(() =>
|
||||||
|
[...rows]
|
||||||
|
.filter((row) => {
|
||||||
|
if (!filterField) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
for (let field of filterField) {
|
||||||
|
for (let value of filterValue) {
|
||||||
|
let eq = (row[field] === value) === filterEqual;
|
||||||
|
if (!eq) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.sort(getComparator(order, orderBy))
|
||||||
|
.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage),
|
||||||
|
[order, orderBy, page, rowsPerPage, rows, filterEqual, filterField, filterValue],
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderTable = (row) => {
|
||||||
|
// const isItemSelected = selected.includes(row.id);
|
||||||
|
const isItemSelected = false;
|
||||||
|
return (
|
||||||
|
<TableRow hover onClick={() => handleSelect(row)} role="checkbox"
|
||||||
|
aria-checked={isItemSelected} tabIndex={-1} key={row.id} selected={isItemSelected}
|
||||||
|
sx={{cursor: 'pointer'}}>
|
||||||
|
{cells.map((cell) => {
|
||||||
|
return (
|
||||||
|
<TableCell key={cell.id} sx={{maxWidth: cell.width}}>{getTableValue(row, cell.id)}</TableCell>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const emptyRow = () => {
|
||||||
|
return (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={cells.length}>
|
||||||
|
Нет заказов
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{width: '100%'}}>
|
||||||
|
<Paper sx={{width: '100%', mb: 2}}>
|
||||||
|
<EnhancedTableToolbar numSelected={0} name={name}/>
|
||||||
|
<TableContainer>
|
||||||
|
<Table sx={{width: 'calc(100% - 30px)'}} aria-labelledby="tableTitle" size="medium">
|
||||||
|
<EnhancedTableHead numSelected={0} order={order} orderBy={orderBy}
|
||||||
|
onRequestSort={handleRequestSort}
|
||||||
|
rowCount={rows.length} cells={cells}/>
|
||||||
|
<TableBody>
|
||||||
|
{visibleRows.map((row) => renderTable(row))}
|
||||||
|
{visibleRows.length === 0 && emptyRow()}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
<TablePagination rowsPerPageOptions={[5, 10, 25]} component="div" count={visibleRows.length}
|
||||||
|
rowsPerPage={rowsPerPage}
|
||||||
|
page={page} onPageChange={handleChangePage}
|
||||||
|
onRowsPerPageChange={handleChangeRowsPerPage}/>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
32
front/src/components/orders/EnhancedTableHead.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import TableHead from "@mui/material/TableHead";
|
||||||
|
import TableRow from "@mui/material/TableRow";
|
||||||
|
import TableCell from "@mui/material/TableCell";
|
||||||
|
import TableSortLabel from "@mui/material/TableSortLabel";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import {visuallyHidden} from "@mui/utils";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
export function EnhancedTableHead(props) {
|
||||||
|
const {order, orderBy, onRequestSort, cells} = props;
|
||||||
|
const createSortHandler = (property) => (event) => {onRequestSort(event, property);};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
{cells.map((headCell) => (
|
||||||
|
<TableCell key={headCell.id} align={"left"} padding={headCell.disablePadding ? 'none' : 'normal'}
|
||||||
|
sortDirection={orderBy === headCell.id ? order : false} sx={{pl: 1, maxWidth: headCell.width}}>
|
||||||
|
<TableSortLabel active={orderBy === headCell.id} direction={orderBy === headCell.id ? order : 'asc'} onClick={createSortHandler(headCell.id)}>
|
||||||
|
{headCell.label}
|
||||||
|
{orderBy === headCell.id ? (
|
||||||
|
<Box component="span" sx={visuallyHidden}>
|
||||||
|
{order === 'desc' ? 'sorted descending' : 'sorted ascending'}
|
||||||
|
</Box>
|
||||||
|
) : null}
|
||||||
|
</TableSortLabel>
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
);
|
||||||
|
}
|
||||||
64
front/src/components/orders/EnhancedTableToolbar.js
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import Toolbar from "@mui/material/Toolbar";
|
||||||
|
import {alpha} from "@mui/material/styles";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import Tooltip from "@mui/material/Tooltip";
|
||||||
|
import IconButton from "@mui/material/IconButton";
|
||||||
|
import DeleteIcon from "@mui/icons-material/Delete";
|
||||||
|
import FilterListIcon from "@mui/icons-material/FilterList";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
export function EnhancedTableToolbar(props) {
|
||||||
|
const { numSelected, name } = props;
|
||||||
|
return (
|
||||||
|
<Toolbar
|
||||||
|
sx={[
|
||||||
|
{
|
||||||
|
pl: { sm: 2 },
|
||||||
|
pr: { xs: 1, sm: 1 },
|
||||||
|
},
|
||||||
|
numSelected > 0 && {
|
||||||
|
bgcolor: (theme) =>
|
||||||
|
alpha(theme.palette.primary.main, theme.palette.action.activatedOpacity),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{numSelected > 0 ? (
|
||||||
|
<Typography
|
||||||
|
sx={{ flex: '1 1 100%' }}
|
||||||
|
color="inherit"
|
||||||
|
variant="subtitle1"
|
||||||
|
component="div"
|
||||||
|
>
|
||||||
|
{numSelected} selected
|
||||||
|
</Typography>
|
||||||
|
) : (
|
||||||
|
<Typography
|
||||||
|
sx={{ flex: '1 1 100%' }}
|
||||||
|
variant="h6"
|
||||||
|
id="tableTitle"
|
||||||
|
component="div"
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
{numSelected > 0 ? (
|
||||||
|
<Tooltip title="Delete">
|
||||||
|
<IconButton>
|
||||||
|
<DeleteIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
<Tooltip title="Filter list">
|
||||||
|
<IconButton>
|
||||||
|
<FilterListIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</Toolbar>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
EnhancedTableToolbar.propTypes = {
|
||||||
|
numSelected: PropTypes.number.isRequired,
|
||||||
|
};
|
||||||
95
front/src/components/orders/OrderModal.js
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import {useEffect, useState} from 'react';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import Dialog from '@mui/material/Dialog';
|
||||||
|
import DialogActions from '@mui/material/DialogActions';
|
||||||
|
import DialogContent from '@mui/material/DialogContent';
|
||||||
|
import DialogContentText from '@mui/material/DialogContentText';
|
||||||
|
import DialogTitle from '@mui/material/DialogTitle';
|
||||||
|
import Stack from "@mui/material/Stack";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import {ButtonGroup} from "@mui/material";
|
||||||
|
import {requests} from "../../requests";
|
||||||
|
import {useAlert} from "../../hooks/useAlert";
|
||||||
|
import {api} from "../../lib/clients/api";
|
||||||
|
|
||||||
|
function renderButtons(row, my, handleChange) {
|
||||||
|
if (my) {
|
||||||
|
if (row.status === "NEW") {
|
||||||
|
return (
|
||||||
|
<ButtonGroup variant="contained">
|
||||||
|
<Button color="error" onClick={() => handleChange(row, "CANCEL")}>Отмена</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<ButtonGroup variant="contained">
|
||||||
|
<Button color="success" onClick={() => handleChange(row, "DONE")}>Выполнен</Button>
|
||||||
|
<Button color="error" onClick={() => handleChange(row, "CANCEL")}>Отмена</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function OrderModal({row, handleClose, open, handleChange, my}) {
|
||||||
|
const [receipt, setReceipt] = useState([]);
|
||||||
|
const {createError} = useAlert();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!row) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
api().get(requests.bar.receipts + row.cocktail.id)
|
||||||
|
.then((r) => setReceipt(r.data))
|
||||||
|
.catch(() => createError("Ошибка получения рецепта"))
|
||||||
|
// eslint-disable-next-line
|
||||||
|
}, [row]);
|
||||||
|
|
||||||
|
if (!row) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
fullWidth={true}
|
||||||
|
maxWidth="350px"
|
||||||
|
open={open}
|
||||||
|
onClose={handleClose}
|
||||||
|
>
|
||||||
|
<DialogTitle>{"Заказ №" + row.id}</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogContentText>{row.cocktail.name}</DialogContentText>
|
||||||
|
<DialogContentText>{row.cocktail.alcoholic + " " + row.cocktail.category}</DialogContentText>
|
||||||
|
<DialogContentText>{"для: " + row.visitor.name + " " + row.visitor.lastName}</DialogContentText>
|
||||||
|
<Box noValidate component="form"
|
||||||
|
sx={{display: 'flex', flexDirection: 'column', m: 'auto', width: 'fit-content',}}>
|
||||||
|
<Stack>
|
||||||
|
<img src={row.cocktail.image} alt={row.cocktail.name} loading={"eager"} width={"300"}/>
|
||||||
|
<Typography>Ингредиенты:</Typography>
|
||||||
|
<Stack pl={1}>
|
||||||
|
{receipt.map((r) => {
|
||||||
|
return (<Typography key={r.id}>{`${r.ingredient.name} - ${r.measure}`}</Typography>)
|
||||||
|
})}
|
||||||
|
</Stack>
|
||||||
|
<Typography>Инструкция:</Typography>
|
||||||
|
<Typography pl={1}>{row.cocktail.instructions}</Typography>
|
||||||
|
|
||||||
|
{row.cocktail.video && (<iframe width="350" /*height="315"*/
|
||||||
|
src={row.cocktail.video}
|
||||||
|
title="YouTube video player"
|
||||||
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||||
|
referrerPolicy="strict-origin-when-cross-origin"
|
||||||
|
allowFullScreen></iframe>)}
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions sx={{justifyContent: "space-between"}}>
|
||||||
|
{renderButtons(row, my, handleChange)}
|
||||||
|
<Button onClick={handleClose}>Close</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
9
front/src/components/orders/createHeadCelll.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export function createHeadCell(id, numeric, padding, label, width) {
|
||||||
|
return {
|
||||||
|
id: id,
|
||||||
|
numeric: numeric,
|
||||||
|
disablePadding: padding,
|
||||||
|
label: label,
|
||||||
|
width: width
|
||||||
|
}
|
||||||
|
}
|
||||||
49
front/src/components/visitor/VisitorItem.js
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import {Card, FormControlLabel} from "@mui/material";
|
||||||
|
import Stack from "@mui/material/Stack";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import Switch from "@mui/material/Switch";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
export function VisitorItem({visitor, changeHandler, open}) {
|
||||||
|
|
||||||
|
const getRole = (role) => {
|
||||||
|
switch (role) {
|
||||||
|
case "USER":
|
||||||
|
return 'Посетитель';
|
||||||
|
case "BARMEN":
|
||||||
|
return 'Бармен';
|
||||||
|
case "ADMIN":
|
||||||
|
return 'Админ';
|
||||||
|
default:
|
||||||
|
return "Посетитель";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card sx={{mb: 1, p: 1, borderRadius: '10px', maxWidth: '600px'}}>
|
||||||
|
<Stack>
|
||||||
|
<Typography variant='h6'>{`${visitor.name} ${!visitor.lastName ? "" : visitor.lastName}`}</Typography>
|
||||||
|
<Box display='flex' justifyContent='flex-end'>
|
||||||
|
<Typography>{getRole(visitor.role)}</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box display='flex' justifyContent='flex-start'>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Switch
|
||||||
|
checked={visitor.invited}
|
||||||
|
disabled={open}
|
||||||
|
onChange={() => changeHandler(visitor)}
|
||||||
|
/>}
|
||||||
|
label="Приглашен" labelPlacement='start'/>
|
||||||
|
</Box>
|
||||||
|
<Box display='flex' justifyContent='flex-end'>
|
||||||
|
<Typography
|
||||||
|
variant='body2'
|
||||||
|
color={visitor.isActive ? 'green' : 'red'}
|
||||||
|
>{visitor.isActive ? "В баре" : "Не вошел в бар"}</Typography>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
49
front/src/context/AuthContext.js
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import {useCallback, useEffect} from 'react';
|
||||||
|
import {logger} from "../lib/DefaultLogger";
|
||||||
|
import {tokenUtil} from "../lib/TokenUtil";
|
||||||
|
|
||||||
|
export const AuthContext = React.createContext(undefined);
|
||||||
|
|
||||||
|
export function AuthProvider({children}) {
|
||||||
|
const [state, setState] = React.useState({
|
||||||
|
auth: false,
|
||||||
|
error: "",
|
||||||
|
isLoading: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const checkSession = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
if (!await tokenUtil.checkToken(tokenUtil.getToken())) {
|
||||||
|
setState((prev) => ({...prev, auth: false, error: '', isLoading: false}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState((prev) => ({...prev, auth: true, error: "", isLoading: false}));
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(err);
|
||||||
|
setState((prev) => ({...prev, auth: false, error: 'Что-то пошло не так', isLoading: false}));
|
||||||
|
}
|
||||||
|
updater().then();
|
||||||
|
// eslint-disable-next-line
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
checkSession()
|
||||||
|
.catch((err) => {
|
||||||
|
logger.error(err);
|
||||||
|
});
|
||||||
|
// eslint-disable-next-line
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const updater = async () => {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000 * 60 * 60));
|
||||||
|
checkSession()
|
||||||
|
.catch((err) => {
|
||||||
|
logger.error(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return <AuthContext.Provider value={{...state}}>{children}</AuthContext.Provider>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AuthConsumer = AuthContext.Consumer;
|
||||||
66
front/src/context/UserContext.js
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import {logger} from "../lib/DefaultLogger";
|
||||||
|
import {userClient} from "../lib/clients/UserClient";
|
||||||
|
import {tokenUtil} from "../lib/TokenUtil";
|
||||||
|
import {createContext, useCallback, useEffect, useState} from "react";
|
||||||
|
import {api} from "../lib/clients/api";
|
||||||
|
import {requests} from "../requests";
|
||||||
|
|
||||||
|
export const UserContext = createContext(undefined);
|
||||||
|
|
||||||
|
export function UserProvider({children}) {
|
||||||
|
const refresh = () => {
|
||||||
|
checkSession()
|
||||||
|
.catch((err) => logger.error(err))
|
||||||
|
}
|
||||||
|
const [state, setState] = useState({
|
||||||
|
user: {},
|
||||||
|
session: {},
|
||||||
|
error: "",
|
||||||
|
isLoading: true,
|
||||||
|
refresh: refresh
|
||||||
|
});
|
||||||
|
|
||||||
|
const checkSession = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
setState((prev) => ({...prev, isLoading: true}));
|
||||||
|
if (!await tokenUtil.checkToken(tokenUtil.getToken())) {
|
||||||
|
setState((prev) => ({...prev, error: '', isLoading: false, user: {}}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
api().get(requests.bar.session.status)
|
||||||
|
.then((r) => setState((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
session: r.data
|
||||||
|
})))
|
||||||
|
.catch(() => setState((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
session: {}
|
||||||
|
})))
|
||||||
|
if (Object.keys(state.user).length === 0) {
|
||||||
|
const {data, errorData} = await userClient.getMe();
|
||||||
|
if (errorData) {
|
||||||
|
setState((prev) => ({...prev, error: errorData, isLoading: false, user: {}}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState((prev) => ({...prev, error: "", isLoading: false, user: data}));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(err);
|
||||||
|
setState((prev) => ({...prev, error: 'Что-то пошло не так', isLoading: false, user: {}}));
|
||||||
|
}
|
||||||
|
}, [state]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
checkSession()
|
||||||
|
.catch((err) => {
|
||||||
|
logger.error(err);
|
||||||
|
});
|
||||||
|
// eslint-disable-next-line
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return <UserContext.Provider value={{...state}}>{children}</UserContext.Provider>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UserConsumer = UserContext.Consumer;
|
||||||
|
|
||||||
32
front/src/hooks/useAlert.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import {useSnackbar} from "notistack";
|
||||||
|
|
||||||
|
export function useAlert() {
|
||||||
|
// variant could be success, error, warning, info, or default
|
||||||
|
const {enqueueSnackbar} = useSnackbar();
|
||||||
|
|
||||||
|
function createAlert(message, variant) {
|
||||||
|
const options = {
|
||||||
|
...variant,
|
||||||
|
anchorOrigin: {vertical: 'top', horizontal: 'right'},
|
||||||
|
}
|
||||||
|
enqueueSnackbar(message, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
function notImplement() {
|
||||||
|
createAlert("Данный функционал пока не реализован", {variant: 'warning'});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createError(message) {
|
||||||
|
createAlert(message, {variant: "error"});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getError() {
|
||||||
|
createAlert("Ошибка получения данных", {variant: "error"});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSuccess(message) {
|
||||||
|
createAlert(message, {variant: "success"});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {createAlert, notImplement, createError, getError, createSuccess}
|
||||||
|
}
|
||||||
14
front/src/hooks/useAuth.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import {AuthContext} from "../context/AuthContext";
|
||||||
|
|
||||||
|
export function useAuth() {
|
||||||
|
const context = React.useContext(AuthContext);
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useAuth must be used within a AuthProvider');
|
||||||
|
}
|
||||||
|
|
||||||
|
window.auth = context;
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
20
front/src/hooks/usePopover.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
export function usePopover() {
|
||||||
|
const anchorRef = React.useRef(null);
|
||||||
|
const [open, setOpen] = React.useState(false);
|
||||||
|
|
||||||
|
const handleOpen = React.useCallback(() => {
|
||||||
|
setOpen(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleClose = React.useCallback(() => {
|
||||||
|
setOpen(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleToggle = React.useCallback(() => {
|
||||||
|
setOpen((prevState) => !prevState);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { anchorRef, handleClose, handleOpen, handleToggle, open };
|
||||||
|
}
|
||||||
47
front/src/hooks/useSelection.js
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
// IMPORTANT: To prevent infinite loop, `keys` argument must be memoized with React.useMemo hook.
|
||||||
|
export function useSelection(keys) {
|
||||||
|
const [selected, setSelected] = React.useState(new Set());
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
setSelected(new Set());
|
||||||
|
}, [keys]);
|
||||||
|
|
||||||
|
const handleDeselectAll = React.useCallback(() => {
|
||||||
|
setSelected(new Set());
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleDeselectOne = React.useCallback((key) => {
|
||||||
|
setSelected((prev) => {
|
||||||
|
const copy = new Set(prev);
|
||||||
|
copy.delete(key);
|
||||||
|
return copy;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSelectAll = React.useCallback(() => {
|
||||||
|
setSelected(new Set(keys));
|
||||||
|
}, [keys]);
|
||||||
|
|
||||||
|
const handleSelectOne = React.useCallback((key) => {
|
||||||
|
setSelected((prev) => {
|
||||||
|
const copy = new Set(prev);
|
||||||
|
copy.add(key);
|
||||||
|
return copy;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const selectedAny = selected.size > 0;
|
||||||
|
const selectedAll = selected.size === keys.length;
|
||||||
|
|
||||||
|
return {
|
||||||
|
deselectAll: handleDeselectAll,
|
||||||
|
deselectOne: handleDeselectOne,
|
||||||
|
selectAll: handleSelectAll,
|
||||||
|
selectOne: handleSelectOne,
|
||||||
|
selected,
|
||||||
|
selectedAny,
|
||||||
|
selectedAll,
|
||||||
|
};
|
||||||
|
}
|
||||||
13
front/src/hooks/useUser.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import {UserContext} from "../context/UserContext";
|
||||||
|
|
||||||
|
export function useUser() {
|
||||||
|
const context = React.useContext(UserContext);
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useUser must be used within a UserProvider');
|
||||||
|
}
|
||||||
|
|
||||||
|
window.user = context;
|
||||||
|
return context;
|
||||||
|
}
|
||||||
13
front/src/index.css
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
13
front/src/index.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import App from './app/App';
|
||||||
|
import './index.css';
|
||||||
|
import '@fontsource/roboto/300.css';
|
||||||
|
import '@fontsource/roboto/400.css';
|
||||||
|
import '@fontsource/roboto/500.css';
|
||||||
|
import '@fontsource/roboto/700.css';
|
||||||
|
|
||||||
|
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||||
|
root.render(
|
||||||
|
<App/>
|
||||||
|
);
|
||||||
4
front/src/lib/DefaultLogger.js
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import {createLogger} from "./Logger";
|
||||||
|
import {config} from "../Config";
|
||||||
|
|
||||||
|
export const logger = createLogger({ level: config.logLevel });
|
||||||
65
front/src/lib/Logger.js
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
/* eslint-disable no-console -- Allow */
|
||||||
|
|
||||||
|
// NOTE: A tracking system such as Sentry should replace the console
|
||||||
|
|
||||||
|
export const LogLevel = {NONE: 'NONE', ERROR: 'ERROR', WARN: 'WARN', DEBUG: 'DEBUG', ALL: 'ALL'};
|
||||||
|
|
||||||
|
const LogLevelNumber = {NONE: 0, ERROR: 1, WARN: 2, DEBUG: 3, ALL: 4};
|
||||||
|
|
||||||
|
export class Logger {
|
||||||
|
prefix;
|
||||||
|
level;
|
||||||
|
showLevel;
|
||||||
|
|
||||||
|
levelNumber;
|
||||||
|
|
||||||
|
constructor({prefix = '', level = LogLevel.ALL, showLevel = true}) {
|
||||||
|
this.prefix = prefix;
|
||||||
|
this.level = level;
|
||||||
|
this.levelNumber = LogLevelNumber[this.level];
|
||||||
|
this.showLevel = showLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
debug = (...args) => {
|
||||||
|
if (this.canWrite(LogLevel.DEBUG)) {
|
||||||
|
this.write(LogLevel.DEBUG, ...args);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
warn = (...args) => {
|
||||||
|
if (this.canWrite(LogLevel.WARN)) {
|
||||||
|
this.write(LogLevel.WARN, ...args);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
error = (...args) => {
|
||||||
|
if (this.canWrite(LogLevel.ERROR)) {
|
||||||
|
this.write(LogLevel.ERROR, ...args);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
canWrite(level) {
|
||||||
|
return this.levelNumber >= LogLevelNumber[level];
|
||||||
|
}
|
||||||
|
|
||||||
|
write(level, ...args) {
|
||||||
|
let prefix = this.prefix;
|
||||||
|
|
||||||
|
if (this.showLevel) {
|
||||||
|
prefix = `- ${level} ${prefix}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (level === LogLevel.ERROR) {
|
||||||
|
console.error(prefix, ...args);
|
||||||
|
} else {
|
||||||
|
console.log(prefix, ...args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This can be extended to create context specific logger (Server Action, Router Handler, etc.)
|
||||||
|
// to add context information (IP, User-Agent, timestamp, etc.)
|
||||||
|
|
||||||
|
export function createLogger({prefix, level} = {}) {
|
||||||
|
return new Logger({prefix, level});
|
||||||
|
}
|
||||||
34
front/src/lib/TokenUtil.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import {decodeToken, isExpired} from "react-jwt";
|
||||||
|
import {requests} from "../requests";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
class TokenUtil {
|
||||||
|
|
||||||
|
checkToken(token) {
|
||||||
|
if (token == null || isExpired(token)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.refreshToken();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
getToken() {
|
||||||
|
return localStorage.getItem("token");
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshToken() {
|
||||||
|
const decoded = decodeToken(this.getToken());
|
||||||
|
const currentTime = Date.now() / 1000;
|
||||||
|
if (decoded.exp - currentTime > 43200) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
axios.post(requests.auth.refresh, {}, {headers: {'Authorization': this.getToken()}})
|
||||||
|
.then((r) => {
|
||||||
|
localStorage.setItem("token", r.data.token)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const tokenUtil = new TokenUtil();
|
||||||
9
front/src/lib/clients/AuthClient.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
class AuthClient {
|
||||||
|
|
||||||
|
async signOut() {
|
||||||
|
localStorage.removeItem("token");
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const authClient = new AuthClient();
|
||||||
17
front/src/lib/clients/UserClient.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import {requests} from "../../requests";
|
||||||
|
import {api} from "./api";
|
||||||
|
|
||||||
|
class UserClient {
|
||||||
|
|
||||||
|
async getMe() {
|
||||||
|
try{
|
||||||
|
let url = requests.users.getMe
|
||||||
|
const response = await api().get(url);
|
||||||
|
return {data: response.data}
|
||||||
|
} catch (e) {
|
||||||
|
return {errorData: e.data}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const userClient = new UserClient();
|
||||||