Initial commit

This commit is contained in:
Kayashov.SM
2025-03-12 17:54:16 +04:00
commit b6d8a3cebd
254 changed files with 29963 additions and 0 deletions

5
front/Dockerfile Normal file
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

66
front/package.json Normal file
View 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"
]
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

View 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

View 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

View 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

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
front/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 385 KiB

43
front/public/index.html Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

BIN
front/public/logo512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View 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
View File

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

7
front/src/Config.js Normal file
View 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
View 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
View 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;

View 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}/>
)
}

View 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/>),
},
]

View 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>
);
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
);
}

View 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}/>
)
}

View File

@@ -0,0 +1,7 @@
import CocktailsPageContent from "./CocktailsPageContent";
export function AllCocktailsPage() {
return (
<CocktailsPageContent all={true}/>
)
}

View 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>
)
}

View 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;

View 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>
)
}

View File

@@ -0,0 +1,7 @@
import CocktailsPageContent from "./CocktailsPageContent";
export function MenuPage() {
return (
<CocktailsPageContent all={false}/>
)
}

View 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>
)
}

View 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>
)
}

View 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>
);
}

View File

@@ -0,0 +1,7 @@
import {QueueContent} from "./QueueContent";
export function MyQueuePage() {
return (
<QueueContent my={true}/>
)
}

View 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}/>
</>
)
}

View File

@@ -0,0 +1,10 @@
import * as React from "react";
import {QueueContent} from "./QueueContent";
const QueuePage = () => {
return (
<QueueContent my={false}/>
)
}
export default QueuePage;

View 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>
)
}

View 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>
);
}

View 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>
)
}

View 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>;
}

View 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>
);
}

View 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>
)
}

View 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>
)
}

View 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>
);
}

View 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>
)
}

View 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>
)
}

View 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',
})
}));

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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: "Сначала не избранные"
// }
]

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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',
},
}));

View 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}
</>
)
}

View 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,
};

View 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,
},
}));

View 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>
</>
);
}

View 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;
}

View 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);
}

View 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;
};

View 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,
}

View File

@@ -0,0 +1,6 @@
export function a11yProps(index) {
return {
id: `simple-tab-${index}`,
'aria-controls': `simple-tabpanel-${index}`,
};
}

View 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}
/>
</>
);
}

View 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>
);
}

View 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>
);
}

View 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}
</>
)
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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,
};

View 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>
);
}

View File

@@ -0,0 +1,9 @@
export function createHeadCell(id, numeric, padding, label, width) {
return {
id: id,
numeric: numeric,
disablePadding: padding,
label: label,
width: width
}
}

View 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>
)
}

View 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;

View 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;

View 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}
}

View 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;
}

View 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 };
}

View 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,
};
}

View 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
View 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
View 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/>
);

View 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
View 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});
}

View 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();

View File

@@ -0,0 +1,9 @@
class AuthClient {
async signOut() {
localStorage.removeItem("token");
return {};
}
}
export const authClient = new AuthClient();

View 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();

Some files were not shown because too many files have changed in this diff Show More