initial commit
This commit is contained in:
7
front/src/Config.js
Normal file
7
front/src/Config.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import {getSiteURL} from "./lib/getSiteUrl";
|
||||
import {LogLevel} from "./lib/Logger";
|
||||
|
||||
export const config = {
|
||||
site: {name: 'Bar', description: '', themeColor: '#090a0b', url: getSiteURL()},
|
||||
logLevel: (process.env.NEXT_PUBLIC_LOG_LEVEL) ?? LogLevel.ALL,
|
||||
};
|
||||
5
front/src/Dockerfile
Normal file
5
front/src/Dockerfile
Normal file
@@ -0,0 +1,5 @@
|
||||
FROM nginx:alpine as nginx
|
||||
WORKDIR /app
|
||||
COPY ../nginx.conf /etc/nginx/nginx.conf
|
||||
EXPOSE 80
|
||||
CMD "nginx" "-g" "daemon off;"
|
||||
55
front/src/app/App.js
Normal file
55
front/src/app/App.js
Normal file
@@ -0,0 +1,55 @@
|
||||
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";
|
||||
import {SelectProvider} from "../context/SelectContext";
|
||||
|
||||
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,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
{/*Провайдер выбора*/}
|
||||
<SelectProvider>
|
||||
{/*Маршрутизация*/}
|
||||
<Router>
|
||||
<NavigationRoutes/>
|
||||
</Router>
|
||||
</SelectProvider>
|
||||
</CssVarsProvider>
|
||||
</UserProvider>
|
||||
</AuthProvider>
|
||||
</SnackbarProvider>
|
||||
</LocalizationProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
10
front/src/app/HomeRedirect.js
Normal file
10
front/src/app/HomeRedirect.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import {paths} from "../path";
|
||||
import {Loading} from "../components/core/Loading";
|
||||
|
||||
export function HomeRedirect({auth}) {
|
||||
const redirectPath = auth ? paths.dashboard.overview : paths.auth.signIn;
|
||||
window.location.replace(redirectPath);
|
||||
return (
|
||||
<Loading loading={true}/>
|
||||
)
|
||||
}
|
||||
132
front/src/app/NavigationRoutes.js
Normal file
132
front/src/app/NavigationRoutes.js
Normal file
@@ -0,0 +1,132 @@
|
||||
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 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 {EditIngredientPage} from "./pages/ingredients/EditIngredientPage";
|
||||
import {EditCocktailPage} from "./pages/cocktails/EditCocktailPage";
|
||||
import {useEffect, useState} from "react";
|
||||
import {BarChangePage} from "./pages/BarChangePage";
|
||||
import {CalcPage} from "./pages/calc/CalcPage";
|
||||
|
||||
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.bar.calc,
|
||||
children: (<CalcPage/>),
|
||||
isPrivate: true,
|
||||
},
|
||||
{
|
||||
path: paths.dashboard.overview,
|
||||
isPrivate: true,
|
||||
children: (<MenuPage/>),
|
||||
exact: true,
|
||||
},
|
||||
{
|
||||
path: paths.bar.list,
|
||||
isPrivate: true,
|
||||
children: (<BarChangePage/>),
|
||||
},
|
||||
{
|
||||
path: paths.bar.ingredients,
|
||||
isPrivate: true,
|
||||
children: (<IngredientsPage/>)
|
||||
},
|
||||
{
|
||||
path: paths.bar.ingredientEdit,
|
||||
isPrivate: true,
|
||||
forAdmin: true,
|
||||
children: (<EditIngredientPage/>)
|
||||
},
|
||||
{
|
||||
path: paths.bar.cocktailEdit,
|
||||
isPrivate: true,
|
||||
forAdmin: true,
|
||||
children: (<EditCocktailPage/>)
|
||||
},
|
||||
{
|
||||
path: paths.notFound,
|
||||
isPrivate: false,
|
||||
children: (<NotFoundPage/>)
|
||||
},
|
||||
]
|
||||
|
||||
const guestPages = [
|
||||
{
|
||||
path: paths.dashboard.overview,
|
||||
isPrivate: true,
|
||||
children: (<MenuPage/>),
|
||||
exact: true,
|
||||
},
|
||||
{
|
||||
children: (<HomeRedirect auth={true}/>),
|
||||
isPrivate: false,
|
||||
path: paths.home,
|
||||
},
|
||||
{
|
||||
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/>),
|
||||
},
|
||||
]
|
||||
55
front/src/app/layout/PublicLayout.js
Normal file
55
front/src/app/layout/PublicLayout.js
Normal file
@@ -0,0 +1,55 @@
|
||||
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>
|
||||
<Box
|
||||
component="img"
|
||||
alt="Under development"
|
||||
src="/assets/qr.png"
|
||||
sx={{ display: 'inline-block', height: 'auto', maxWidth: '100%', width: '400px' }}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
31
front/src/app/layout/UserLayout.js
Normal file
31
front/src/app/layout/UserLayout.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import {SideNav} from "../../components/navigation/SideNav";
|
||||
import Box from "@mui/material/Box";
|
||||
import {MainNav} from "../../components/navigation/MainNav";
|
||||
import Container from "@mui/material/Container";
|
||||
|
||||
export function UserLayout({children}) {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
bgcolor: 'var(--mui-palette-background-default)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
position: 'relative',
|
||||
minHeight: '100%',
|
||||
}}
|
||||
>
|
||||
<SideNav/>
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
flex: '1 1 auto',
|
||||
flexDirection: 'column',
|
||||
pl: {xl: 'var(--SideNav-width)'}
|
||||
}}>
|
||||
<MainNav/>
|
||||
<Container maxWidth="xl" sx={{py: '16px'}}>
|
||||
{children}
|
||||
</Container>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
79
front/src/app/pages/BarChangePage.js
Normal file
79
front/src/app/pages/BarChangePage.js
Normal file
@@ -0,0 +1,79 @@
|
||||
import Paper from "@mui/material/Paper";
|
||||
import React, {useEffect, useState} from "react";
|
||||
import {useAlert} from "../../hooks/useAlert";
|
||||
import {Card} from "@mui/material";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import Box from "@mui/material/Box";
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import ElectricalServicesIcon from '@mui/icons-material/ElectricalServices';
|
||||
import Toolbar from "@mui/material/Toolbar";
|
||||
import AddCircleIcon from '@mui/icons-material/AddCircle';
|
||||
import {BarCreateModal} from "../../components/BarCreateModal";
|
||||
import PowerIcon from '@mui/icons-material/Power';
|
||||
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
||||
import {barClient} from "../../lib/clients/BarClient";
|
||||
|
||||
export function BarChangePage() {
|
||||
const [bars, setBars] = useState([])
|
||||
const [open, setOpen] = useState(false)
|
||||
const [oldId, setOldId] = useState(null);
|
||||
const {createError, createSuccess, createWarning} = useAlert();
|
||||
|
||||
const createHandler = (id, name) => {
|
||||
if (id) {
|
||||
barClient.copyBar(id, name, setBars, bars, createError, createSuccess, setOpen)
|
||||
} else {
|
||||
barClient.createBar(name, bars, createSuccess, createError, setBars, setOpen)
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line
|
||||
useEffect(() => barClient.getBarList(setBars, createError), []);
|
||||
|
||||
return (<>
|
||||
<BarCreateModal open={open} setOpen={setOpen} create={createHandler} id={oldId}/>
|
||||
<Paper sx={{p: 1}}>
|
||||
<Toolbar>
|
||||
<Typography variant='h6'>Списки ингредиентов (бары)</Typography>
|
||||
<IconButton edge="end" onClick={() => {
|
||||
setOldId(null);
|
||||
setOpen(true);
|
||||
}}>
|
||||
<AddCircleIcon/>
|
||||
</IconButton>
|
||||
</Toolbar>
|
||||
{bars.map((b) => {
|
||||
return <Card key={b.id} sx={{m: 2, p: 2}}>
|
||||
<Stack direction='row' justifyContent={'space-between'}>
|
||||
<Typography>{b.name}</Typography>
|
||||
<Box>
|
||||
<IconButton onClick={() => {
|
||||
setOldId(b.id)
|
||||
setOpen(true);
|
||||
}}>
|
||||
<ContentCopyIcon/>
|
||||
</IconButton>
|
||||
{b.active && <IconButton disabled>
|
||||
<PowerIcon/>
|
||||
</IconButton>}
|
||||
{!b.active && <>
|
||||
<IconButton
|
||||
onClick={() => barClient.deleteBar(b, bars, createError, createSuccess, setBars)}>
|
||||
<DeleteIcon/>
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={() => barClient.changeBar(b.id, bars, createWarning, createSuccess, createError, setBars)}>
|
||||
<ElectricalServicesIcon/>
|
||||
</IconButton>
|
||||
</>}
|
||||
</Box>
|
||||
</Stack>
|
||||
</Card>
|
||||
})}
|
||||
</Paper>
|
||||
</>
|
||||
|
||||
)
|
||||
}
|
||||
11
front/src/app/pages/auth/sign-in/loginPage.js
Normal file
11
front/src/app/pages/auth/sign-in/loginPage.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import * as React from 'react';
|
||||
import {GuestGuard} from "../../../../components/auth/guest-guard";
|
||||
import {SignInForm} from "../../../../components/auth/sign-in-form";
|
||||
|
||||
export default function LoginPage() {
|
||||
return (
|
||||
<GuestGuard>
|
||||
<SignInForm/>
|
||||
</GuestGuard>
|
||||
);
|
||||
}
|
||||
16
front/src/app/pages/auth/sign-in/telegram-code.js
Normal file
16
front/src/app/pages/auth/sign-in/telegram-code.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import * as React from "react";
|
||||
import {useSearchParams} from "react-router-dom";
|
||||
import {Loading} from "../../../../components/core/Loading";
|
||||
import {useAuth} from "../../../../hooks/useAuth";
|
||||
import {authClient} from "../../../../lib/clients/AuthClient";
|
||||
|
||||
export function TelegramCode() {
|
||||
const [searchParams] = useSearchParams();
|
||||
const {checkSession} = useAuth();
|
||||
|
||||
authClient.loginByCode(searchParams.get("code"), checkSession)
|
||||
|
||||
return (
|
||||
<Loading loading={true}/>
|
||||
)
|
||||
}
|
||||
90
front/src/app/pages/calc/CalcPage.js
Normal file
90
front/src/app/pages/calc/CalcPage.js
Normal file
@@ -0,0 +1,90 @@
|
||||
import Typography from "@mui/material/Typography";
|
||||
import * as React from "react";
|
||||
import {useEffect, useMemo} from "react";
|
||||
import {useAlert} from "../../../hooks/useAlert";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Box from "@mui/material/Box";
|
||||
import {CocktailItemCalc} from "./CocktailItemCalc";
|
||||
import {IngredientCalcCard} from "./IngredientCalcCard";
|
||||
import {cocktailClient} from "../../../lib/clients/CocktailClient";
|
||||
|
||||
export function CalcPage() {
|
||||
const {createError} = useAlert();
|
||||
const [cocktails, setCocktails] = React.useState([]);
|
||||
const [load, setLoad] = React.useState(false);
|
||||
const [cocktailMap, setCocktailMap] = React.useState({});
|
||||
|
||||
const changeHandler = (id, value) => {
|
||||
setCocktailMap((prev) => ({
|
||||
...prev,
|
||||
[id]: value
|
||||
}));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
cocktailClient.getCocktailsForCalcPage(load, setLoad, setCocktails, setCocktailMap, createError)
|
||||
// eslint-disable-next-line
|
||||
}, [load]);
|
||||
|
||||
const ingredients = useMemo(() => {
|
||||
let map = {}
|
||||
if (!cocktails) {
|
||||
return [];
|
||||
}
|
||||
cocktails.forEach((c) => {
|
||||
const receipts = c.receipt;
|
||||
const countMeter = cocktailMap[c.id];
|
||||
|
||||
if (!receipts) {
|
||||
return
|
||||
}
|
||||
receipts.forEach((r) => {
|
||||
const ingredient = r.ingredient;
|
||||
const id = ingredient.id;
|
||||
const ingredientCount = r.count;
|
||||
|
||||
const resultCount = ingredientCount * countMeter;
|
||||
|
||||
if (map[id]) {
|
||||
map[id] = {
|
||||
...map[id],
|
||||
count: map[id].count + resultCount
|
||||
}
|
||||
} else {
|
||||
map[id] = {
|
||||
ingredient: ingredient,
|
||||
count: resultCount
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
return Object.values(map);
|
||||
},
|
||||
[cocktails, cocktailMap])
|
||||
|
||||
const deleteHandler = (id) => {
|
||||
const state = cocktails.filter((c) => c.id !== id);
|
||||
setCocktails(state);
|
||||
}
|
||||
|
||||
console.log(cocktailMap)
|
||||
|
||||
return (
|
||||
<Box padding={2}>
|
||||
<Typography variant="h4" align="center">Коктейли</Typography>
|
||||
<Stack mt={2}>
|
||||
{cocktails.map((item, i) => (
|
||||
<CocktailItemCalc key={i} cocktail={item} deleteHandler={deleteHandler}
|
||||
changeHandler={changeHandler}/>
|
||||
))}
|
||||
</Stack>
|
||||
|
||||
<Typography variant="h4" mt={2} align="center">Ингредиенты</Typography>
|
||||
<Stack mt={2}>
|
||||
{ingredients.map((item, i) => (
|
||||
<IngredientCalcCard key={i} count={item.count} ingredient={item.ingredient}/>
|
||||
))}
|
||||
</Stack>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
40
front/src/app/pages/calc/CocktailItemCalc.js
Normal file
40
front/src/app/pages/calc/CocktailItemCalc.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import {Card} from "@mui/material";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Box from "@mui/material/Box";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import DeleteIcon from "@mui/icons-material/Delete";
|
||||
import React from "react";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import {Counter} from "./Counter";
|
||||
|
||||
export function CocktailItemCalc({cocktail, deleteHandler, changeHandler}) {
|
||||
return (
|
||||
<Card sx={{mb: 1, display: 'relative', p: 2}}>
|
||||
<Stack justifyContent={'start'} spacing={2}>
|
||||
<Stack direction='row' justifyContent='start' alignItems='center'>
|
||||
<Box sx={{width: '100px', height: '100px'}}>
|
||||
<img src={cocktail.image} loading='lazy' height={'100px'} width={'100px'} alt={cocktail.id}/>
|
||||
</Box>
|
||||
<Box sx={{width: 'calc(90% - 100px)', pr: 2, ml: 2}}>
|
||||
<Stack>
|
||||
<Typography>{cocktail.name}</Typography>
|
||||
<Typography>{cocktail.volume}</Typography>
|
||||
<Typography>{cocktail.category}</Typography>
|
||||
<Typography>{cocktail.alcoholic}</Typography>
|
||||
<Typography color={'textSecondary'}>{cocktail.components}</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
<Stack direction='row'>
|
||||
<Stack sx={{width: '5%'}} spacing={1} justifyContent='flex-start'>
|
||||
<IconButton size='small' onClick={() => deleteHandler(cocktail.id)}>
|
||||
<DeleteIcon/>
|
||||
</IconButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Counter id={cocktail.id} changeHandler={changeHandler}/>
|
||||
</Stack>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
65
front/src/app/pages/calc/Counter.js
Normal file
65
front/src/app/pages/calc/Counter.js
Normal file
@@ -0,0 +1,65 @@
|
||||
import React, {useState} from 'react';
|
||||
import {Box, TextField, Button} from '@mui/material';
|
||||
import {styled} from '@mui/material/styles';
|
||||
|
||||
// Стилизуем контейнер счетчика
|
||||
styled(Box)`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 150px;
|
||||
height: 50px;
|
||||
border-radius: 8px;
|
||||
//box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
//background-color: #ffffff;
|
||||
`;
|
||||
|
||||
export function Counter({id, changeHandler}) {
|
||||
const [value, setValue] = useState(1);
|
||||
|
||||
const handleChange = (newValue) => {
|
||||
setValue(newValue);
|
||||
changeHandler(id, newValue);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Button onClick={() => {
|
||||
if (value > 0) {
|
||||
setValue(value - 1);
|
||||
}
|
||||
}}
|
||||
sx={{
|
||||
width: '20px',
|
||||
height: '55px',
|
||||
borderRadius: '50%',
|
||||
margin: '0 8px',
|
||||
backgroundColor: 'transparent',
|
||||
}}>−</Button>
|
||||
<TextField
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
const newValue = parseInt(e.target.value, 10);
|
||||
if (!isNaN(newValue)) {
|
||||
handleChange(newValue);
|
||||
}
|
||||
}}
|
||||
inputProps={{inputMode: 'numeric'}}
|
||||
sx={{
|
||||
width: '40px',
|
||||
height: '15px',
|
||||
fontSize: '10px',
|
||||
textAlign: 'center'
|
||||
}}
|
||||
/>
|
||||
<Button onClick={() => handleChange(value + 1)}
|
||||
sx={{
|
||||
width: '20px',
|
||||
height: '55px',
|
||||
borderRadius: '50%',
|
||||
margin: '0 8px',
|
||||
backgroundColor: 'transparent',
|
||||
}}>+</Button>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
21
front/src/app/pages/calc/IngredientCalcCard.js
Normal file
21
front/src/app/pages/calc/IngredientCalcCard.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import {Card} from "@mui/material";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Box from "@mui/material/Box";
|
||||
import React from "react";
|
||||
|
||||
export function IngredientCalcCard({ingredient, count}) {
|
||||
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={ingredient.image} loading='lazy' height={'100px'} width={'100px'} alt={ingredient.id}/>
|
||||
</Box>
|
||||
<Box sx={{width: 'calc(90% - 100px)', pr: 2}}>{ingredient.name}</Box>
|
||||
<Stack direction='row'>
|
||||
<Box mr={1} pt={'3px'}>{count}</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Card>
|
||||
)
|
||||
|
||||
}
|
||||
196
front/src/app/pages/cocktails/CocktailsPageContent.js
Normal file
196
front/src/app/pages/cocktails/CocktailsPageContent.js
Normal file
@@ -0,0 +1,196 @@
|
||||
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 {NoResult} from "../../../components/cocktails/NoResult";
|
||||
import {FilterBlock} from "../../../components/cocktails/FilterBlock";
|
||||
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 {useSelect} from "../../../hooks/useSelect";
|
||||
import Paper from "@mui/material/Paper";
|
||||
import CheckMarks from "../../../components/cocktails/CheckMarks";
|
||||
import {cocktailClient} from "../../../lib/clients/CocktailClient";
|
||||
|
||||
const emptyFilter = {
|
||||
search: "",
|
||||
all: false,
|
||||
hidden: true,
|
||||
onlyFavourite: false,
|
||||
glass: [],
|
||||
category: [],
|
||||
alcohol: [],
|
||||
iCount: [],
|
||||
ingredient: [],
|
||||
inMenu: "",
|
||||
sorting: "Название по возрастанию"
|
||||
}
|
||||
|
||||
const CocktailsPageContent = () => {
|
||||
const {user} = useUser();
|
||||
const {createError, createSuccess} = useAlert();
|
||||
const [rows, setRows] = useState([]);
|
||||
const [filter, setFilter] = useState(emptyFilter)
|
||||
// const [chips, setChips] = useState([])
|
||||
const chips = [];
|
||||
const [page, setPage] = useState(-1);
|
||||
const [load, setLoad] = useState(false);
|
||||
const [isEnd, setIsEnd] = useState(false);
|
||||
const [isNew, setIsNew] = useState(true);
|
||||
|
||||
const {selectCocktail, getCocktail, getOpenCocktail} = useSelect();
|
||||
|
||||
const loading = useCallback(() => {
|
||||
const size = Math.floor((window.innerWidth) / 350) * 5;
|
||||
if (load || (!isNew && isEnd)) {
|
||||
return false;
|
||||
}
|
||||
cocktailClient.getMenu(setRows, setIsNew, setPage, setLoad, setIsEnd, isNew, rows, page, size, filter, createError);
|
||||
// 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);
|
||||
// eslint-disable-next-line
|
||||
}, [loading]);
|
||||
// eslint-disable-next-line
|
||||
useEffect(() => loading(), [filter])
|
||||
|
||||
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;
|
||||
})
|
||||
cocktailClient.changeRating(row.id, newState, value, setRows, createSuccess, 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;
|
||||
});
|
||||
cocktailClient.changeFavourite(value, row.id, newState, setRows, createSuccess, createError)
|
||||
}
|
||||
const handleFilterClear = () => {
|
||||
setFilter(emptyFilter);
|
||||
setIsNew(true);
|
||||
setIsEnd(false);
|
||||
setPage(-1);
|
||||
}
|
||||
|
||||
const handleSelectCocktail = (row) => selectCocktail(row.id)
|
||||
const deleteHandle = (row) => cocktailClient.deleteCocktail(row.id, rows, setRows, createSuccess, createError)
|
||||
const hideHandler = (id) => {
|
||||
cocktailClient.hiddenCocktail(id)
|
||||
.then(() => {
|
||||
createSuccess("Коктейль скрыт успешно");
|
||||
setRows(rows.filter((r) => r.id !== id))
|
||||
}).catch(() => createError("Ошибка при попытке скрыть коктейль"))
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{/*<Loading loading={load}/>*/}
|
||||
{/*Модальное окно информации о коктейле*/}
|
||||
<CocktailInfoModal row={getCocktail()} open={getOpenCocktail()}/>
|
||||
{/*Блок фильтров*/}
|
||||
<FilterBlock
|
||||
filter={filter}
|
||||
handleFilterChange={handleFilterChange}
|
||||
handleClearFilter={handleFilterClear}
|
||||
barmen={user.role !== 'USER'}
|
||||
/>
|
||||
|
||||
{
|
||||
(filter.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}
|
||||
deleteHandler={deleteHandle}
|
||||
hideHandler={hideHandler}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
{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;
|
||||
194
front/src/app/pages/cocktails/EditCocktailPage.js
Normal file
194
front/src/app/pages/cocktails/EditCocktailPage.js
Normal file
@@ -0,0 +1,194 @@
|
||||
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 {useAlert} from "../../../hooks/useAlert";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Button from "@mui/material/Button";
|
||||
import {EditCocktailReceipt} from "../../../components/cocktails/EditCocktailReceipt";
|
||||
import {SelectEdit} from "../../../components/cocktails/SelectEdit";
|
||||
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";
|
||||
import {cocktailClient} from "../../../lib/clients/CocktailClient";
|
||||
import {categoryClient} from "../../../lib/clients/CategoryClient";
|
||||
import {glassClient} from "../../../lib/clients/GlassClient";
|
||||
|
||||
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([]);
|
||||
|
||||
useEffect(() => {
|
||||
cocktailClient.getSimpleList(setCocktails, setSelected, setLoading, createError, searchParams.get("id"))
|
||||
categoryClient.getCategoryList(setCategory, createError);
|
||||
glassClient.getGlassList(setGlass, createError)
|
||||
// eslint-disable-next-line
|
||||
}, []);
|
||||
|
||||
// eslint-disable-next-line
|
||||
useEffect(() => cocktailClient.getOneCocktail(selected, setCocktail, getError, emptyCocktail), [selected])
|
||||
const saveHandler = () => cocktailClient.saveChangeCocktail(cocktail, createError, createSuccess)
|
||||
const deleteHandle = () => cocktailClient.deleteCocktailFromEdit(setCocktails, setCocktail, createError, cocktails, cocktail, emptyCocktail)
|
||||
|
||||
const changeCocktailValue = (name, value) => {
|
||||
if (name === "tags") {
|
||||
value = value.join(",");
|
||||
}
|
||||
setCocktail((prev) => ({
|
||||
...prev,
|
||||
[name]: value
|
||||
}))
|
||||
}
|
||||
|
||||
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) => cocktailClient.savePhoto(event, changeCocktailValue, 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}/>
|
||||
</Box>
|
||||
{/*Рецепт*/}
|
||||
<EditCocktailReceipt receipt={cocktail.receipt} handler={changeCocktailValue}/>
|
||||
|
||||
<Box pr={2} ml={1}>
|
||||
<TextField sx={{width: '100%'}}
|
||||
label={"Инструкция"} variant='outlined' multiline
|
||||
value={!cocktail.instructions ? "" : cocktail.instructions}
|
||||
onChange={(e) => changeCocktailValue("instructions", e.target.value)}
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Paper>
|
||||
<Box display={'flex'} justifyContent={'flex-end'}>
|
||||
<Button variant='contained' onClick={() => saveHandler()}>Сохранить</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
7
front/src/app/pages/cocktails/MenuPage.js
Normal file
7
front/src/app/pages/cocktails/MenuPage.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import CocktailsPageContent from "./CocktailsPageContent";
|
||||
|
||||
export function MenuPage() {
|
||||
return (
|
||||
<CocktailsPageContent/>
|
||||
)
|
||||
}
|
||||
142
front/src/app/pages/ingredients/EditIngredientPage.js
Normal file
142
front/src/app/pages/ingredients/EditIngredientPage.js
Normal file
@@ -0,0 +1,142 @@
|
||||
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 {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 {ingredientClient} from "../../../lib/clients/IngredientClient";
|
||||
|
||||
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(() => {
|
||||
ingredientClient.allList(searchParams.get("id"), setIngredients, setIngredient, createError)
|
||||
ingredientClient.getType(setTypes)
|
||||
// eslint-disable-next-line
|
||||
}, []);
|
||||
|
||||
const changeIngredientValue = (name, value) => {
|
||||
setIngredient((prev) => ({
|
||||
...prev,
|
||||
[name]: value
|
||||
}))
|
||||
}
|
||||
|
||||
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) => {
|
||||
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)}/>
|
||||
</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
|
||||
onChange={(e) => changeIngredientValue("description", e.target.value)}
|
||||
value={!ingredient.description ? "" : ingredient.description}/>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Paper>
|
||||
<Box display={'flex'} justifyContent={'flex-end'}>
|
||||
<Button variant='contained'
|
||||
onClick={() => ingredientClient.saveIngredient(ingredient, createSuccess, createError)}>Сохранить</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
126
front/src/app/pages/ingredients/IngredientsPage.js
Normal file
126
front/src/app/pages/ingredients/IngredientsPage.js
Normal file
@@ -0,0 +1,126 @@
|
||||
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, 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 {useAlert} from "../../../hooks/useAlert";
|
||||
import {IngredientInfoModal} from "../../../components/Ingredients/IngredientInfoModal";
|
||||
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 {useSelect} from "../../../hooks/useSelect";
|
||||
import {ingredientClient} from "../../../lib/clients/IngredientClient";
|
||||
|
||||
export function IngredientsPage() {
|
||||
const [value, setValue] = React.useState(0);
|
||||
const handleChange = (event, newValue) => setValue(newValue);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [findString, setFindString] = useState("");
|
||||
const [ingredients, setIngredients] = useState([]);
|
||||
const {getIngredient, selectIngredient} = useSelect();
|
||||
const {createError, createSuccess} = useAlert();
|
||||
|
||||
useEffect(() => {
|
||||
ingredientClient.getAllIngredients(setIngredients, setLoading, createError)
|
||||
// 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;
|
||||
}
|
||||
})
|
||||
ingredientClient.changeIngredientIsHave(row.id, value, newState, setIngredients, createError)
|
||||
}
|
||||
const handleDelete = (id) => {
|
||||
const newState = ingredients.filter((ingredient) => ingredient.id !== id);
|
||||
setIngredients(newState)
|
||||
ingredientClient.deleteIngredientIsHave(id, createSuccess, createError)
|
||||
}
|
||||
|
||||
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}>
|
||||
<IngredientList rows={ingredientsInBar} value={false} changeHandler={changeHandler}
|
||||
infoHandler={selectIngredient}/>
|
||||
</CustomTabPanel>
|
||||
<CustomTabPanel value={value} index={1}>
|
||||
<IngredientList rows={ingredientsToAdd} value={true} changeHandler={changeHandler}
|
||||
infoHandler={selectIngredient}/>
|
||||
</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={getIngredient()} handleDelete={handleDelete}/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
38
front/src/app/pages/notFound/NotFoundPage.js
Normal file
38
front/src/app/pages/notFound/NotFoundPage.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import * as React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft';
|
||||
import {paths} from "../../../path";
|
||||
|
||||
export default function NotFoundPage() {
|
||||
return (
|
||||
<Box component="main" sx={{ alignItems: 'center', display: 'flex', justifyContent: 'center', minHeight: '100%' }}>
|
||||
<Stack spacing={3} sx={{ alignItems: 'center', maxWidth: 'md' }}>
|
||||
<Box>
|
||||
<Box
|
||||
component="img"
|
||||
alt="Under development"
|
||||
src="/assets/error-404.png"
|
||||
sx={{ display: 'inline-block', height: 'auto', maxWidth: '100%', width: '400px' }}
|
||||
/>
|
||||
</Box>
|
||||
<Typography variant="h3" sx={{ textAlign: 'center' }}>
|
||||
404: Страница не найдена или недоступна
|
||||
</Typography>
|
||||
<Typography color="text.secondary" variant="body1" sx={{ textAlign: 'center' }}>
|
||||
Вы либо выбрали какой-то сомнительный маршрут, либо попали сюда по ошибке. Что бы это ни было, попробуйте воспользоваться навигацией
|
||||
</Typography>
|
||||
<Button
|
||||
// component={'a'}
|
||||
href={paths.home}
|
||||
startIcon={<ArrowLeftIcon fontSize="var(--icon-fontSize-md)" />}
|
||||
variant="contained"
|
||||
>
|
||||
На домашнюю страницу
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
43
front/src/components/BarCreateModal.js
Normal file
43
front/src/components/BarCreateModal.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import Dialog from "@mui/material/Dialog";
|
||||
import DialogTitle from "@mui/material/DialogTitle";
|
||||
import * as React from "react";
|
||||
import {useState} from "react";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import DialogContent from "@mui/material/DialogContent";
|
||||
import DialogActions from "@mui/material/DialogActions";
|
||||
import Button from "@mui/material/Button";
|
||||
import TextField from "@mui/material/TextField";
|
||||
|
||||
export function BarCreateModal({open, setOpen, create, id}) {
|
||||
const [value, setValue] = useState("");
|
||||
return (
|
||||
<Dialog fullWidth={true}
|
||||
open={open} onClose={() => setOpen(false)}
|
||||
sx={{
|
||||
'& .MuiDialog-paper': {
|
||||
margin: '8px',
|
||||
},
|
||||
'& .MuiPaper-root': {
|
||||
width: 'calc(100% - 16px)',
|
||||
}
|
||||
}}>
|
||||
<DialogTitle>
|
||||
<Typography>Создать список</Typography>
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<TextField sx={{width: '75%'}}
|
||||
label={<Typography pt={'4px'}>
|
||||
Название списка</Typography>} variant='outlined'
|
||||
value={!value ? "" : value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => {
|
||||
create(id, value);
|
||||
setValue("");
|
||||
}}>Создать</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
39
front/src/components/Ingredients/IngredientAlert.js
Normal file
39
front/src/components/Ingredients/IngredientAlert.js
Normal file
@@ -0,0 +1,39 @@
|
||||
import * as React from 'react';
|
||||
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';
|
||||
|
||||
export function IngredientAlert({open, handleClose, handleDelete, id, handleCloseParent}) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
aria-labelledby="Предупреждение об удалении"
|
||||
aria-describedby="alert-dialog-description"
|
||||
>
|
||||
<DialogTitle id="alert-dialog-title">
|
||||
{"Вы готовы удалить ингредиент?"}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText id="alert-dialog-description">
|
||||
После удаления ингредиента, удаляться все рецепты и коктейли связанные с ним!
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleClose}>Отмена</Button>
|
||||
<Button color='error' onClick={() => {
|
||||
handleClose();
|
||||
handleCloseParent();
|
||||
handleDelete(id)
|
||||
}} autoFocus>
|
||||
Удалить
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
37
front/src/components/Ingredients/IngredientCard.js
Normal file
37
front/src/components/Ingredients/IngredientCard.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import {Card} from "@mui/material";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import AddBoxRoundedIcon from '@mui/icons-material/AddBoxRounded';
|
||||
import InfoRoundedIcon from '@mui/icons-material/InfoRounded';
|
||||
import React from "react";
|
||||
import Box from "@mui/material/Box";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import {paths} from "../../path";
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
|
||||
export function IngredientCard({row, value, infoHandler, changeHandler}) {
|
||||
return (
|
||||
<Card sx={{mb: 1, height: '130px', display: 'relative', pt: 1}}>
|
||||
<Stack direction='row' justifyContent='start' alignItems='center'>
|
||||
<Box sx={{width: '100px', height: '100px'}}>
|
||||
<img src={row.image} loading='lazy' 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>
|
||||
)
|
||||
}
|
||||
85
front/src/components/Ingredients/IngredientInfoModal.js
Normal file
85
front/src/components/Ingredients/IngredientInfoModal.js
Normal file
@@ -0,0 +1,85 @@
|
||||
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 {useEffect, useState} from "react";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import List from "@mui/material/List";
|
||||
import {useAlert} from "../../hooks/useAlert";
|
||||
import ListItem from "@mui/material/ListItem";
|
||||
import {useSelect} from "../../hooks/useSelect";
|
||||
import {IngredientAlert} from "./IngredientAlert";
|
||||
import {useUser} from "../../hooks/useUser";
|
||||
import {cocktailClient} from "../../lib/clients/CocktailClient";
|
||||
|
||||
export function IngredientInfoModal({ingredient, handleDelete}) {
|
||||
const {user} = useUser();
|
||||
const [cocktails, setCocktails] = useState([]);
|
||||
const {closeIngredient, getOpenIngredient, selectCocktail} = useSelect();
|
||||
const {createError} = useAlert();
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
const handleClose = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
cocktailClient.getCocktailByIngredient(ingredient, setCocktails)
|
||||
.catch(() => createError())
|
||||
// eslint-disable-next-line
|
||||
}, [ingredient]);
|
||||
|
||||
if (!ingredient) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Dialog fullWidth={true} maxWidth="350px" open={getOpenIngredient()} onClose={closeIngredient}
|
||||
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>
|
||||
{cocktails.length > 0 && (
|
||||
<>
|
||||
<Typography sx={{mt: 2}}>Коктейли:</Typography>
|
||||
<List>
|
||||
{cocktails.map((c) => {
|
||||
return (
|
||||
<ListItem key={c.id} onClick={() => {
|
||||
selectCocktail(c.id)
|
||||
closeIngredient();
|
||||
}}>
|
||||
<Stack direction={'row'}>
|
||||
<img src={c.image} alt={c.name} loading={"eager"} width={"50"}/>
|
||||
<Typography sx={{mx: 1}}>{c.name}</Typography>
|
||||
{c.rating.rating > 0 && <Typography> {`${c.rating.rating}/5`}</Typography>}
|
||||
</Stack>
|
||||
</ListItem>
|
||||
)
|
||||
})}
|
||||
</List>
|
||||
</>
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
{user.role !== 'USER' && <Button onClick={() => setOpen(true)}>Удалить</Button>}
|
||||
<Button onClick={closeIngredient}>Закрыть</Button>
|
||||
</DialogActions>
|
||||
<IngredientAlert handleDelete={handleDelete} handleClose={handleClose} open={open} id={ingredient.id}
|
||||
handleCloseParent={closeIngredient}/>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
55
front/src/components/Ingredients/IngredientList.js
Normal file
55
front/src/components/Ingredients/IngredientList.js
Normal file
@@ -0,0 +1,55 @@
|
||||
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}) {
|
||||
const visibleRows = useMemo(() => {
|
||||
let res = [];
|
||||
if (rows.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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
|
||||
}, [rows])
|
||||
|
||||
return (
|
||||
<Box mt={2}>
|
||||
{visibleRows}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
48
front/src/components/auth/guest-guard.js
Normal file
48
front/src/components/auth/guest-guard.js
Normal file
@@ -0,0 +1,48 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import Alert from '@mui/material/Alert';
|
||||
import {useAuth} from "../../hooks/useAuth";
|
||||
import {logger} from "../../lib/DefaultLogger";
|
||||
import {paths} from "../../path";
|
||||
|
||||
export function GuestGuard({ children }) {
|
||||
const { auth, error, isLoading } = useAuth();
|
||||
const [isChecking, setIsChecking] = React.useState(true);
|
||||
|
||||
const checkPermissions = async () => {
|
||||
if (isLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
setIsChecking(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (auth) {
|
||||
logger.debug('[GuestGuard]: User is logged in, redirecting to dashboard');
|
||||
window.location.replace(paths.dashboard.overview);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsChecking(false);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
checkPermissions().catch(() => {
|
||||
// noop
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- Expected
|
||||
}, [auth, error, isLoading]);
|
||||
|
||||
if (isChecking) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <Alert color="error">{error}</Alert>;
|
||||
}
|
||||
|
||||
return <React.Fragment>{children}</React.Fragment>;
|
||||
}
|
||||
122
front/src/components/auth/sign-in-form.js
Normal file
122
front/src/components/auth/sign-in-form.js
Normal file
@@ -0,0 +1,122 @@
|
||||
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 {useAuth} from "../../hooks/useAuth";
|
||||
import {authClient} from "../../lib/clients/AuthClient";
|
||||
|
||||
const emptyRequest = {
|
||||
byLogin: true,
|
||||
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(true)
|
||||
const {checkSession} = useAuth();
|
||||
|
||||
const buttonSx = {
|
||||
minWidth: "300px",
|
||||
...(error && {
|
||||
bgcolor: red[500],
|
||||
'&:hover': {
|
||||
bgcolor: red[700],
|
||||
},
|
||||
}),
|
||||
};
|
||||
const handleButtonClick = async () => {
|
||||
await authClient.login(request, setLoading, setError, checkSession)
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
68
front/src/components/cocktails/CheckMarks.js
Normal file
68
front/src/components/cocktails/CheckMarks.js
Normal file
@@ -0,0 +1,68 @@
|
||||
import * as React from 'react';
|
||||
import OutlinedInput from '@mui/material/OutlinedInput';
|
||||
import InputLabel from '@mui/material/InputLabel';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import FormControl from '@mui/material/FormControl';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import Select from '@mui/material/Select';
|
||||
import Checkbox from '@mui/material/Checkbox';
|
||||
|
||||
const ITEM_HEIGHT = 48;
|
||||
const ITEM_PADDING_TOP = 8;
|
||||
const MenuProps = {
|
||||
PaperProps: {
|
||||
style: {
|
||||
maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP,
|
||||
width: 250,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default function CheckMarks({rows, name, filterValue, handleChange, filterName, width, nonMulti, nullValue, identity}) {
|
||||
const realValue = !nonMulti ? filterValue.filter((v) => v.length > 0) : filterValue;
|
||||
return (
|
||||
<div>
|
||||
<FormControl sx={{m: 1, width: !width ? 300 : width}}>
|
||||
<InputLabel>{name}</InputLabel>
|
||||
<Select
|
||||
multiple={!nonMulti}
|
||||
value={realValue}
|
||||
onChange={(e) => handleChange(filterName, e.target.value)}
|
||||
input={<OutlinedInput label={name}/>}
|
||||
renderValue={(selected) => !nonMulti ? selected.join(", ") : selected}
|
||||
MenuProps={MenuProps}
|
||||
defaultChecked={nonMulti && rows[0]}
|
||||
variant="filled">
|
||||
{(nonMulti && nullValue) && (
|
||||
<MenuItem value={""}>
|
||||
<em>Не выбрано</em>
|
||||
</MenuItem>
|
||||
)}
|
||||
{rows.map((value) => {
|
||||
if(identity) {
|
||||
return (
|
||||
<MenuItem key={"menuItemIn" + value} value={value}>
|
||||
{!nonMulti && (
|
||||
<Checkbox
|
||||
checked={realValue.includes(value)}/>
|
||||
)}
|
||||
<ListItemText primary={value}/>
|
||||
</MenuItem>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<MenuItem key={value.id} value={value.name}>
|
||||
{!nonMulti && (
|
||||
<Checkbox
|
||||
checked={realValue.includes(value.name)}/>
|
||||
)}
|
||||
<ListItemText primary={value.name}/>
|
||||
</MenuItem>
|
||||
)
|
||||
}
|
||||
})}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
95
front/src/components/cocktails/Cocktail.js
Normal file
95
front/src/components/cocktails/Cocktail.js
Normal file
@@ -0,0 +1,95 @@
|
||||
import {CardActions, CardContent, CardMedia, Rating} from "@mui/material";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Grid from "@mui/material/Grid";
|
||||
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 Box from "@mui/material/Box";
|
||||
import {CocktailDescription} from "./CocktailDescription";
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import LocalBarIcon from '@mui/icons-material/LocalBar';
|
||||
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
|
||||
import {paths} from "../../path";
|
||||
import {useAlert} from "../../hooks/useAlert";
|
||||
import {useUser} from "../../hooks/useUser";
|
||||
import {cocktailClient} from "../../lib/clients/CocktailClient";
|
||||
|
||||
function renderFavouriteBadge(handleFavourite, row) {
|
||||
const childIcon = row.rating.favourite ? <FavoriteIcon/> : <FavoriteBorderIcon/>;
|
||||
return (
|
||||
<IconButton size={'small'}
|
||||
onClick={() => handleFavourite(row)}>
|
||||
{childIcon}
|
||||
</IconButton>
|
||||
)
|
||||
}
|
||||
|
||||
function renderRating(handleChangeRating, row) {
|
||||
return (
|
||||
<Rating
|
||||
name="simple-controlled"
|
||||
value={row.rating.rating}
|
||||
onChange={(event, newValue) => handleChangeRating(row, newValue)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function Cocktail({row, handleFavourite, handleChangeRating, handleSelect, deleteHandler, hideHandler}) {
|
||||
const {createError, createSuccess} = useAlert();
|
||||
const {user} = useUser();
|
||||
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.includes("thecocktaildb") ? (row.image + "/preview") : row.image}
|
||||
/>
|
||||
<CardActions>
|
||||
<IconButton sx={{m: 0}} size='small'
|
||||
onClick={() => cocktailClient.drinkCocktail(row.id, createSuccess, createError)}>
|
||||
<LocalBarIcon fontSize='small'/>
|
||||
</IconButton>
|
||||
{renderFavouriteBadge(handleFavourite, row)}
|
||||
{renderRating(handleChangeRating, row)}
|
||||
|
||||
{
|
||||
(user.role && user.role !== 'USER') &&
|
||||
<>
|
||||
<IconButton size='small' href={`${paths.bar.cocktailEdit}?id=${row.id}`}>
|
||||
<EditIcon fontSize='small'/>
|
||||
</IconButton>
|
||||
<IconButton size='small' onClick={() => hideHandler(row.id)}>
|
||||
<VisibilityOffIcon fontSize='small'/>
|
||||
</IconButton>
|
||||
<IconButton size='small' onClick={() => deleteHandler(row)}>
|
||||
<DeleteIcon fontSize='small'/>
|
||||
</IconButton>
|
||||
</>
|
||||
|
||||
}
|
||||
</CardActions>
|
||||
<CardContent sx={{pb: 0, pl: 2, pt: 0}}>
|
||||
<Typography variant="h5" minHeight={'50px'} mt={2}>{row.name} </Typography>
|
||||
<CocktailDescription row={row}/>
|
||||
</CardContent>
|
||||
</Box>
|
||||
</CocktailItemStyled>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
32
front/src/components/cocktails/CocktailDescription.js
Normal file
32
front/src/components/cocktails/CocktailDescription.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import ListItem from "@mui/material/ListItem";
|
||||
import ListItemText from "@mui/material/ListItemText";
|
||||
import List from "@mui/material/List";
|
||||
|
||||
export function CocktailDescription({row}) {
|
||||
return (
|
||||
<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>
|
||||
</List>
|
||||
)
|
||||
}
|
||||
124
front/src/components/cocktails/CocktailInfoModal.js
Normal file
124
front/src/components/cocktails/CocktailInfoModal.js
Normal file
@@ -0,0 +1,124 @@
|
||||
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 {useAlert} from "../../hooks/useAlert";
|
||||
import {paths} from "../../path";
|
||||
import {Loading} from "../core/Loading";
|
||||
import {useUser} from "../../hooks/useUser";
|
||||
import {useSelect} from "../../hooks/useSelect";
|
||||
import {cocktailClient} from "../../lib/clients/CocktailClient";
|
||||
import {ingredientClient} from "../../lib/clients/IngredientClient";
|
||||
|
||||
export function CocktailInfoModal({row}) {
|
||||
const {user} = useUser();
|
||||
const {getError, createError, createSuccess} = useAlert();
|
||||
const [cocktail, setCocktail] = useState(null)
|
||||
const [loading, setLoading] = useState(false);
|
||||
const {closeCocktail, selectIngredient, getIngredient, getOpenCocktail} = useSelect();
|
||||
|
||||
// eslint-disable-next-line
|
||||
useEffect(() => cocktailClient.getCocktailForModal(row, setLoading, setCocktail, closeCocktail, getError), [row]);
|
||||
|
||||
if (!row || !cocktail) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Dialog fullWidth={true}
|
||||
open={getOpenCocktail()} onClose={closeCocktail}
|
||||
sx={{
|
||||
'& .MuiDialog-paper': {
|
||||
margin: '8px',
|
||||
},
|
||||
'& .MuiPaper-root': {
|
||||
width: 'calc(100% - 16px)',
|
||||
}
|
||||
}}>
|
||||
<IngredientInfoModal ingredient={getIngredient()}/>
|
||||
<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.includes("thecocktaildb") ? (cocktail.image + "/preview") : cocktail.image}
|
||||
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)
|
||||
return (
|
||||
<Stack key={r.ingredient.id} direction='row' justifyContent={'space-between'}
|
||||
mt={1}>
|
||||
<Stack direction='row'>
|
||||
{(user.role && user.role !== "USER") && (
|
||||
<IconButton size="small" sx={{pb: "2px"}}
|
||||
onClick={() => ingredientClient.changeIngredientInBar(r.ingredient, cocktail, setCocktail, createSuccess, createError)}>
|
||||
{r.ingredient.isHave
|
||||
? (<DeleteIcon fontSize="small"/>)
|
||||
: (<ShoppingCartIcon fontSize="small"/>)
|
||||
}
|
||||
</IconButton>
|
||||
)}
|
||||
<Typography
|
||||
onClick={() => ingredientClient.findOne(r.ingredient.id, selectIngredient, createError)}>{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 && user.role.includes("ADMIN")) && (
|
||||
<Button href={`${paths.bar.cocktailEdit}?id=${cocktail.id}`}>Редактировать</Button>
|
||||
)}
|
||||
<Button onClick={closeCocktail}>Закрыть</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
12
front/src/components/cocktails/CocktailItemStyled.js
Normal file
12
front/src/components/cocktails/CocktailItemStyled.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import {styled} from "@mui/material/styles";
|
||||
import {Card} from "@mui/material";
|
||||
|
||||
export const CocktailItemStyled = styled(Card)(({theme}) => ({
|
||||
backgroundColor: '#fff',
|
||||
...theme.typography.body2,
|
||||
padding: theme.spacing(1),
|
||||
color: theme.palette.text.secondary,
|
||||
...theme.applyStyles('dark', {
|
||||
backgroundColor: '#1A2027',
|
||||
})
|
||||
}));
|
||||
44
front/src/components/cocktails/CocktailListCard.js
Normal file
44
front/src/components/cocktails/CocktailListCard.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import {Card, FormControlLabel} from "@mui/material";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Box from "@mui/material/Box";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import InfoRoundedIcon from "@mui/icons-material/InfoRounded";
|
||||
import {paths} from "../../path";
|
||||
import EditIcon from "@mui/icons-material/Edit";
|
||||
import React from "react";
|
||||
import Switch from "@mui/material/Switch";
|
||||
|
||||
export function CocktailListCard({row, changeHandler, infoHandler}) {
|
||||
return (
|
||||
<Card sx={{mb: 1, height: '130px', display: 'relative', pt: 1, borderRadius: '15px'}}>
|
||||
<Stack direction='row' justifyContent='start' alignItems='start'>
|
||||
<Box sx={{width: '100px', height: '100px', ml: 1}}>
|
||||
<img src={row.image} loading='eager' height={'100px'} width={'100px'} alt={row.id}
|
||||
style={{borderRadius: '20%'}}/>
|
||||
</Box>
|
||||
<Stack sx={{width: 'calc(95% - 100px)', pr: 2, ml: 1}}>
|
||||
<Box>{row.name}</Box>
|
||||
<FormControlLabel sx={{mt: 5, pr: 2}}
|
||||
onClick={() => changeHandler(row, !row.inMenu)}
|
||||
value="bottom"
|
||||
control={
|
||||
<Switch color="primary" checked={row.inMenu}/>
|
||||
}
|
||||
label="В меню"
|
||||
labelPlacement="start"
|
||||
/>
|
||||
</Stack>
|
||||
<Stack direction='row'>
|
||||
<Stack sx={{width: '5%', mt: 2}} spacing={1} justifyContent='flex-start'>
|
||||
<IconButton size='small' onClick={() => infoHandler(row)}>
|
||||
<InfoRoundedIcon/>
|
||||
</IconButton>
|
||||
<IconButton size='small' href={`${paths.bar.cocktailEdit}?id=${row.id}`}>
|
||||
<EditIcon/>
|
||||
</IconButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
76
front/src/components/cocktails/CocktailsList.js
Normal file
76
front/src/components/cocktails/CocktailsList.js
Normal file
@@ -0,0 +1,76 @@
|
||||
import {useMemo, useState} from "react";
|
||||
import {getComparator} from "../core/getComparator";
|
||||
import {Accordion, AccordionDetails, AccordionSummary} from "@mui/material";
|
||||
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Box from "@mui/material/Box";
|
||||
import {CocktailListCard} from "./CocktailListCard";
|
||||
import {groupByForLoop} from "../core/groupByForLoop";
|
||||
|
||||
export function CocktailsList({rows, grouping, changeHandler, infoHandler}) {
|
||||
const [size, setSize] = useState(20);
|
||||
|
||||
window.addEventListener('scroll', () => {
|
||||
if (window.innerHeight + window.scrollY >= (document.documentElement.scrollHeight - 100)) {
|
||||
if (!grouping) {
|
||||
setSize(size + 10)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const visibleRows = useMemo(() => {
|
||||
let res = [];
|
||||
if (rows.length === 0) {
|
||||
return null;
|
||||
}
|
||||
if (!grouping) {
|
||||
return rows
|
||||
.sort(getComparator("asc", "name"))
|
||||
.slice(0, size)
|
||||
.map((row) => {
|
||||
return (
|
||||
<CocktailListCard row={row} key={row.id}
|
||||
changeHandler={changeHandler} infoHandler={infoHandler}/>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const group = groupByForLoop(rows, "category")
|
||||
if (!group || group.size === 0) {
|
||||
return null;
|
||||
}
|
||||
Array.from(group.keys())
|
||||
.sort(getComparator())
|
||||
.map((key) => {
|
||||
const list = group.get(key);
|
||||
res.push(
|
||||
<Accordion key={key} sx={{borderRadius: '5px', mb: 1}}>
|
||||
<AccordionSummary
|
||||
expandIcon={<ExpandMoreIcon/>}
|
||||
aria-controls="panel1-content"
|
||||
id="panel1-header"
|
||||
>
|
||||
<Typography component="span">{key}</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails sx={{p: 1}}>
|
||||
{list.sort(getComparator("asc", "name"))
|
||||
.map((row) => {
|
||||
return (
|
||||
<CocktailListCard row={row} key={row.id}
|
||||
changeHandler={changeHandler} infoHandler={infoHandler}/>
|
||||
)
|
||||
})}
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
)
|
||||
})
|
||||
return res;
|
||||
// eslint-disable-next-line
|
||||
}, [size, rows])
|
||||
|
||||
return (
|
||||
<Box mt={2}>
|
||||
{visibleRows}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
162
front/src/components/cocktails/EditCocktailReceipt.js
Normal file
162
front/src/components/cocktails/EditCocktailReceipt.js
Normal file
@@ -0,0 +1,162 @@
|
||||
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 {Card} from "@mui/material";
|
||||
import {SelectEdit} from "./SelectEdit";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import DeleteForeverIcon from '@mui/icons-material/DeleteForever';
|
||||
import {ingredientClient} from "../../lib/clients/IngredientClient";
|
||||
|
||||
export function EditCocktailReceipt({receipt, handler}) {
|
||||
const {createError} = useAlert()
|
||||
const [ingredients, setIngredients] = useState([]);
|
||||
const [units, setUnits] = useState([])
|
||||
|
||||
useEffect(() => {
|
||||
ingredientClient.findAll(setIngredients, createError)
|
||||
ingredientClient.findUnit(setUnits, createError)
|
||||
// eslint-disable-next-line
|
||||
}, []);
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
||||
105
front/src/components/cocktails/FilterBlock.js
Normal file
105
front/src/components/cocktails/FilterBlock.js
Normal file
@@ -0,0 +1,105 @@
|
||||
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 {useAlert} from "../../hooks/useAlert";
|
||||
import {categoryClient} from "../../lib/clients/CategoryClient";
|
||||
import {glassClient} from "../../lib/clients/GlassClient";
|
||||
|
||||
export function FilterBlock({filter, handleFilterChange, handleClearFilter, barmen}) {
|
||||
const {createError} = useAlert();
|
||||
const [glass, setGlass] = useState([]);
|
||||
const [category, setCategory] = useState([]);
|
||||
const alcohol = ['Алкогольный', 'Безалкогольный'];
|
||||
const ingredientCount = [1, 2, 3, 4, 5];
|
||||
const sort = ['Название по возрастанию', 'Название по убыванию'];
|
||||
|
||||
useEffect(() => {
|
||||
categoryClient.getCategoryList(setCategory, createError)
|
||||
glassClient.getGlassList(setGlass, 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={sort} name={"Сортировать по..."} handleChange={handleFilterChange}
|
||||
filterValue={filter.sorting} filterName={"sorting"} nonMulti identity/>
|
||||
</Grid>
|
||||
</Box>
|
||||
{/*Блок фильтров*/}
|
||||
<Box hidden={filter.hidden}>
|
||||
<Grid container>
|
||||
{/*Фильтр по избранным*/}
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch inputProps={{'aria-label': 'controlled'}}
|
||||
onChange={() => handleFilterChange("onlyFavourite", !filter.onlyFavourite)}
|
||||
/>
|
||||
}
|
||||
label="Только избранные"
|
||||
sx={{ml: 1}}
|
||||
/>
|
||||
{/*Фильтр по избранным*/}
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch inputProps={{'aria-label': 'controlled'}}
|
||||
onChange={() => handleFilterChange("all", !filter.all)}
|
||||
/>
|
||||
}
|
||||
label={!filter.all ? "Полный список" : "Только доступные"}
|
||||
sx={{ml: 1}}
|
||||
/>
|
||||
{/*Фильтр по алкогольности*/}
|
||||
<CheckMarks rows={alcohol} name={"Алкогольность"} handleChange={handleFilterChange}
|
||||
filterValue={filter.alcohol} filterName={"alcohol"} identity/>
|
||||
{/*Фильтр по категории*/}
|
||||
{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"}/>)}
|
||||
{/*Фильтр по нехватке ингредиентов*/}
|
||||
{(barmen && filter.all) && (<CheckMarks rows={ingredientCount} name={"Не хватает ингредиентов"}
|
||||
handleChange={handleFilterChange}
|
||||
nonMulti nullValue identity
|
||||
filterValue={filter.iCount} filterName={"iCount"}/>)}
|
||||
<Button onClick={() => handleClearFilter()}>Сбросить</Button>
|
||||
</Grid>
|
||||
</Box>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
17
front/src/components/cocktails/NoResult.js
Normal file
17
front/src/components/cocktails/NoResult.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import Grid from "@mui/material/Grid";
|
||||
import {Stack} from "@mui/material";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import {CocktailItemStyled} from "./CocktailItemStyled";
|
||||
|
||||
export function NoResult({load}) {
|
||||
return (
|
||||
<Grid item lg={4} md={6} sm={12} xl={3} hidden={!load}>
|
||||
<CocktailItemStyled>
|
||||
<Stack align="center" sx={{width: "350px"}}>
|
||||
<Typography variant="h5" minHeight={'50px'} mt={2}>Нет результатов</Typography>
|
||||
<Typography>Попробуйте заглянуть позднее</Typography>
|
||||
</Stack>
|
||||
</CocktailItemStyled>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
26
front/src/components/cocktails/SelectEdit.js
Normal file
26
front/src/components/cocktails/SelectEdit.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import {FormControl, InputLabel} from "@mui/material";
|
||||
import Select from "@mui/material/Select";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import * as React from "react";
|
||||
|
||||
export function SelectEdit({label, value, array, handler, attributeName, width, margin}) {
|
||||
|
||||
return (
|
||||
<FormControl sx={{width: width, m: margin}}>
|
||||
<InputLabel>{label}</InputLabel>
|
||||
<Select
|
||||
autoWidth
|
||||
label={label}
|
||||
value={!value ? "" : value}
|
||||
onChange={(e) => handler(attributeName, e.target.value)}
|
||||
>
|
||||
<MenuItem value="">
|
||||
<em>None</em>
|
||||
</MenuItem>
|
||||
{array.map((c) => {
|
||||
return (<MenuItem key={c.id} value={c.name}>{c.name}</MenuItem>)
|
||||
})}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)
|
||||
}
|
||||
27
front/src/components/cocktails/sortingList.js
Normal file
27
front/src/components/cocktails/sortingList.js
Normal file
@@ -0,0 +1,27 @@
|
||||
export const sortList = [
|
||||
{
|
||||
id: "name|asc",
|
||||
name: "Название по возрастанию"
|
||||
},
|
||||
{
|
||||
id: "name|desc",
|
||||
name: "Название по убыванию"
|
||||
},
|
||||
// todo: добавить сортировки в беке
|
||||
// {
|
||||
// id: "rating.rating|desc",
|
||||
// name: "Сначала с оценкой"
|
||||
// },
|
||||
// {
|
||||
// id: "rating.rating|asc",
|
||||
// name: "Сначала без оценки"
|
||||
// },
|
||||
// {
|
||||
// id: "rating.favourite|desc",
|
||||
// name: "Сначала избранные"
|
||||
// },
|
||||
// {
|
||||
// id: "rating.favourite|asc",
|
||||
// name: "Сначала не избранные"
|
||||
// }
|
||||
]
|
||||
12
front/src/components/core/Loading.js
Normal file
12
front/src/components/core/Loading.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import {Backdrop, CircularProgress} from "@mui/material";
|
||||
|
||||
export function Loading({loading}) {
|
||||
return (
|
||||
<Backdrop
|
||||
sx={{color: '#fff', zIndex: (theme) => theme.zIndex.drawer + 1}}
|
||||
open={loading}
|
||||
>
|
||||
<CircularProgress color="inherit"/>
|
||||
</Backdrop>
|
||||
);
|
||||
}
|
||||
10
front/src/components/core/LocalizationProvider.js
Normal file
10
front/src/components/core/LocalizationProvider.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import {AdapterDayjs} from '@mui/x-date-pickers/AdapterDayjs';
|
||||
import {LocalizationProvider as Provider} from '@mui/x-date-pickers/LocalizationProvider';
|
||||
import * as React from 'react';
|
||||
import 'dayjs/locale/ru'
|
||||
|
||||
export function LocalizationProvider({children}) {
|
||||
return (
|
||||
<Provider dateAdapter={AdapterDayjs} adapterLocale="ru-Ru">{children}</Provider>
|
||||
);
|
||||
}
|
||||
38
front/src/components/core/Logo.js
Normal file
38
front/src/components/core/Logo.js
Normal file
@@ -0,0 +1,38 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import { useColorScheme } from '@mui/material/styles';
|
||||
import {NoSsr} from "./NoSsr";
|
||||
|
||||
const HEIGHT = 60;
|
||||
const WIDTH = 60;
|
||||
|
||||
export function Logo({ color = 'dark', emblem, height = HEIGHT, width = WIDTH }) {
|
||||
let url;
|
||||
|
||||
if (emblem) {
|
||||
url = color === 'light' ? '/assets/logo-emblem.svg' : '/assets/logo-emblem--dark.svg';
|
||||
} else {
|
||||
url = color === 'light' ? '/assets/logo.svg' : '/assets/logo--dark.svg';
|
||||
}
|
||||
|
||||
return <Box alt="logo" component="img" height={height} src={url} width={width} />;
|
||||
}
|
||||
|
||||
export function DynamicLogo({
|
||||
colorDark = 'light',
|
||||
colorLight = 'dark',
|
||||
height = HEIGHT,
|
||||
width = WIDTH,
|
||||
...props
|
||||
}) {
|
||||
const { colorScheme } = useColorScheme();
|
||||
const color = colorScheme === 'dark' ? colorDark : colorLight;
|
||||
|
||||
return (
|
||||
<NoSsr fallback={<Box sx={{ height: `${height}px`, width: `${width}px` }} />}>
|
||||
<Logo color={color} height={height} width={width} {...props} />
|
||||
</NoSsr>
|
||||
);
|
||||
}
|
||||
25
front/src/components/core/NoSsr.js
Normal file
25
front/src/components/core/NoSsr.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import * as React from 'react';
|
||||
import useEnhancedEffect from '@mui/utils/useEnhancedEffect';
|
||||
|
||||
export function NoSsr(props) {
|
||||
const {children, defer = false, fallback = null} = props;
|
||||
const [mountedState, setMountedState] = React.useState(false);
|
||||
|
||||
useEnhancedEffect(() => {
|
||||
if (!defer) {
|
||||
setMountedState(true);
|
||||
}
|
||||
}, [defer]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (defer) {
|
||||
setMountedState(true);
|
||||
}
|
||||
}, [defer]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{mountedState ? children : fallback}
|
||||
</>
|
||||
)
|
||||
}
|
||||
24
front/src/components/core/TabPanel.js
Normal file
24
front/src/components/core/TabPanel.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import PropTypes from "prop-types";
|
||||
import * as React from "react";
|
||||
|
||||
export function CustomTabPanel(props) {
|
||||
const {children, value, index, ...other} = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
role="tabpanel"
|
||||
hidden={value !== index}
|
||||
id={`simple-tabpanel-${index}`}
|
||||
aria-labelledby={`simple-tab-${index}`}
|
||||
{...other}
|
||||
>
|
||||
{value === index && children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
CustomTabPanel.propTypes = {
|
||||
children: PropTypes.node,
|
||||
index: PropTypes.number.isRequired,
|
||||
value: PropTypes.number.isRequired,
|
||||
};
|
||||
81
front/src/components/core/ThemeSwitch.js
Normal file
81
front/src/components/core/ThemeSwitch.js
Normal file
@@ -0,0 +1,81 @@
|
||||
import {styled, useColorScheme} from "@mui/material/styles";
|
||||
import Switch from "@mui/material/Switch";
|
||||
import React from "react";
|
||||
import Box from "@mui/material/Box";
|
||||
import Typography from "@mui/material/Typography";
|
||||
|
||||
export function ThemeSwitch() {
|
||||
const {mode, setMode} = useColorScheme();
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
alignItems: 'center',
|
||||
// backgroundColor: 'var(--mui-palette-neutral-950)',
|
||||
border: '1px solid var(--mui-palette-neutral-700)',
|
||||
borderRadius: '12px',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
p: '4px 12px',
|
||||
}}
|
||||
>
|
||||
<StyledSwitch
|
||||
checked={mode === 'dark'}
|
||||
onChange={(e) => setMode(e.target.checked ? 'dark' : 'light')}
|
||||
inputProps={{'aria-label': 'controlled'}}
|
||||
/>
|
||||
<Box sx={{flex: '1 1 auto'}}>
|
||||
<Typography color="inherit" variant="subtitle1">
|
||||
{(mode === 'dark' ? "Темная " : "Светлая ") + "тема"}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
const StyledSwitch = styled(Switch)(({theme}) => ({
|
||||
width: 62,
|
||||
height: 34,
|
||||
padding: 7,
|
||||
'& .MuiSwitch-switchBase': {
|
||||
margin: 1,
|
||||
padding: 0,
|
||||
transform: 'translateX(6px)',
|
||||
'&.Mui-checked': {
|
||||
color: '#fff',
|
||||
transform: 'translateX(22px)',
|
||||
'& .MuiSwitch-thumb:before': {
|
||||
backgroundImage: `url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 20 20"><path fill="${encodeURIComponent(
|
||||
'#fff',
|
||||
)}" d="M4.2 2.5l-.7 1.8-1.8.7 1.8.7.7 1.8.6-1.8L6.7 5l-1.9-.7-.6-1.8zm15 8.3a6.7 6.7 0 11-6.6-6.6 5.8 5.8 0 006.6 6.6z"/></svg>')`,
|
||||
},
|
||||
'& + .MuiSwitch-track': {
|
||||
opacity: 1,
|
||||
backgroundColor: theme.palette.mode === 'dark' ? '#8796A5' : '#aab4be',
|
||||
},
|
||||
},
|
||||
},
|
||||
'& .MuiSwitch-thumb': {
|
||||
backgroundColor: theme.palette.mode === 'dark' ? '#003892' : '#001e3c',
|
||||
width: 32,
|
||||
height: 32,
|
||||
'&::before': {
|
||||
content: "''",
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
left: 0,
|
||||
top: 0,
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundPosition: 'center',
|
||||
backgroundImage: `url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 20 20"><path fill="${encodeURIComponent(
|
||||
'#fff',
|
||||
)}" d="M9.305 1.667V3.75h1.389V1.667h-1.39zm-4.707 1.95l-.982.982L5.09 6.072l.982-.982-1.473-1.473zm10.802 0L13.927 5.09l.982.982 1.473-1.473-.982-.982zM10 5.139a4.872 4.872 0 00-4.862 4.86A4.872 4.872 0 0010 14.862 4.872 4.872 0 0014.86 10 4.872 4.872 0 0010 5.139zm0 1.389A3.462 3.462 0 0113.471 10a3.462 3.462 0 01-3.473 3.472A3.462 3.462 0 016.527 10 3.462 3.462 0 0110 6.528zM1.665 9.305v1.39h2.083v-1.39H1.666zm14.583 0v1.39h2.084v-1.39h-2.084zM5.09 13.928L3.616 15.4l.982.982 1.473-1.473-.982-.982zm9.82 0l-.982.982 1.473 1.473.982-.982-1.473-1.473zM9.305 16.25v2.083h1.389V16.25h-1.39z"/></svg>')`,
|
||||
},
|
||||
},
|
||||
'& .MuiSwitch-track': {
|
||||
opacity: 1,
|
||||
backgroundColor: theme.palette.mode === 'dark' ? '#8796A5' : '#aab4be',
|
||||
borderRadius: 20 / 2,
|
||||
},
|
||||
}));
|
||||
101
front/src/components/core/UserPopover.js
Normal file
101
front/src/components/core/UserPopover.js
Normal file
@@ -0,0 +1,101 @@
|
||||
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 {SignIn as SignInIcon} from '@phosphor-icons/react/dist/ssr/SignIn';
|
||||
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";
|
||||
import {paths} from "../../path";
|
||||
|
||||
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);
|
||||
}
|
||||
// eslint-disable-next-line
|
||||
}, [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>*/}
|
||||
{!user.name ? <MenuItem onClick={() => window.location.replace(paths.auth.signIn)}>
|
||||
<ListItemIcon>
|
||||
<SignInIcon fontSize="var(--icon-fontSize-md)"/>
|
||||
</ListItemIcon>
|
||||
Вход
|
||||
</MenuItem> :
|
||||
<MenuItem onClick={handleSignOut}>
|
||||
<ListItemIcon>
|
||||
<SignOutIcon fontSize="var(--icon-fontSize-md)"/>
|
||||
</ListItemIcon>
|
||||
Выход
|
||||
</MenuItem>
|
||||
}
|
||||
</MenuList>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
function userDescriptor(user) {
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
if (!user.name) {
|
||||
return (<Typography variant="subtitle1">Гость</Typography>);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Typography variant="subtitle1">{user.name + " " + user.lastName}</Typography>
|
||||
<Typography color="text.secondary" variant="body2">{user.id}</Typography>
|
||||
</>
|
||||
);
|
||||
}
|
||||
29
front/src/components/core/descendingComparator.js
Normal file
29
front/src/components/core/descendingComparator.js
Normal file
@@ -0,0 +1,29 @@
|
||||
export function descendingComparator(a, b, orderBy, lastOrder) {
|
||||
if (getValue(b, orderBy) < getValue(a, orderBy)) {
|
||||
return -1;
|
||||
}
|
||||
if (getValue(b, orderBy) > getValue(a, orderBy)) {
|
||||
return 1;
|
||||
}
|
||||
if (lastOrder && orderBy !== lastOrder) {
|
||||
if (getValue(b, lastOrder) < getValue(a, lastOrder)) {
|
||||
return 1;
|
||||
}
|
||||
if (getValue(b, lastOrder) > getValue(a, lastOrder)) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function getValue(obj, orderBy) {
|
||||
if (!orderBy) {
|
||||
return obj;
|
||||
}
|
||||
const split = orderBy.split(".")
|
||||
let res = obj[split[0]];
|
||||
for (let i = 1; i < split.length; i++) {
|
||||
res = res[split[i]];
|
||||
}
|
||||
return res;
|
||||
}
|
||||
10
front/src/components/core/getComparator.js
Normal file
10
front/src/components/core/getComparator.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import {descendingComparator} from "./descendingComparator";
|
||||
|
||||
export function getComparator(order, orderBy, lastOrder) {
|
||||
if(!order) {
|
||||
order = "asc"
|
||||
}
|
||||
return order === 'desc'
|
||||
? (a, b) => descendingComparator(a, b, orderBy, lastOrder)
|
||||
: (a, b) => -descendingComparator(a, b, orderBy, lastOrder);
|
||||
}
|
||||
14
front/src/components/core/groupByForLoop.js
Normal file
14
front/src/components/core/groupByForLoop.js
Normal file
@@ -0,0 +1,14 @@
|
||||
export const groupByForLoop = (arr, prop) => {
|
||||
const grouped = new Map();
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
let key = arr[i][prop];
|
||||
if (!key) {
|
||||
key = "Без категории"
|
||||
}
|
||||
if (!grouped.has(key)) {
|
||||
grouped.set(key, []);
|
||||
}
|
||||
grouped.get(key).push(arr[i]);
|
||||
}
|
||||
return grouped;
|
||||
};
|
||||
39
front/src/components/core/navIcons.js
Normal file
39
front/src/components/core/navIcons.js
Normal file
@@ -0,0 +1,39 @@
|
||||
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,
|
||||
Calculator
|
||||
} 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,
|
||||
'calc': Calculator,
|
||||
'x-square': XSquare,
|
||||
user: UserIcon,
|
||||
users: UsersIcon,
|
||||
}
|
||||
6
front/src/components/core/tabProps.js
Normal file
6
front/src/components/core/tabProps.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export function a11yProps(index) {
|
||||
return {
|
||||
id: `simple-tab-${index}`,
|
||||
'aria-controls': `simple-tabpanel-${index}`,
|
||||
};
|
||||
}
|
||||
55
front/src/components/navigation/MainNav.js
Normal file
55
front/src/components/navigation/MainNav.js
Normal file
@@ -0,0 +1,55 @@
|
||||
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 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}>
|
||||
<Avatar onClick={userPopover.handleOpen} ref={userPopover.anchorRef} src="/assets/avatar.png"
|
||||
sx={{cursor: 'pointer'}}/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
<UserPopover anchorEl={userPopover.anchorRef.current} onClose={userPopover.handleClose}
|
||||
open={userPopover.open}/>
|
||||
<MobileNav
|
||||
onClose={() => {
|
||||
setOpenNav(false);
|
||||
}}
|
||||
open={openNav}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
38
front/src/components/navigation/MobileNav.js
Normal file
38
front/src/components/navigation/MobileNav.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import * as React from 'react';
|
||||
import Drawer from '@mui/material/Drawer';
|
||||
import {NavigationMenu} from "./NavigationMenu";
|
||||
|
||||
//Боковое меню
|
||||
export function MobileNav({open, onClose}) {
|
||||
return (
|
||||
<Drawer
|
||||
PaperProps={{
|
||||
sx: {
|
||||
'--MobileNav-background': 'var(--mui-palette-neutral-950)',
|
||||
'--MobileNav-color': 'var(--mui-palette-common-white)',
|
||||
'--NavItem-color': 'var(--mui-palette-neutral-300)',
|
||||
'--NavItem-hover-background': 'rgba(255, 255, 255, 0.04)',
|
||||
'--NavItem-active-background': 'var(--mui-palette-primary-main)',
|
||||
'--NavItem-active-color': 'var(--mui-palette-primary-contrastText)',
|
||||
'--NavItem-disabled-color': 'var(--mui-palette-neutral-500)',
|
||||
'--NavItem-icon-color': 'var(--mui-palette-neutral-400)',
|
||||
'--NavItem-icon-active-color': 'var(--mui-palette-primary-contrastText)',
|
||||
'--NavItem-icon-disabled-color': 'var(--mui-palette-neutral-600)',
|
||||
bgcolor: 'var(--MobileNav-background)',
|
||||
color: 'var(--MobileNav-color)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
maxWidth: '100%',
|
||||
scrollbarWidth: 'none',
|
||||
width: 'var(--MobileNav-width)',
|
||||
zIndex: 'var(--MobileNav-zIndex)',
|
||||
'&::-webkit-scrollbar': {display: 'none'},
|
||||
},
|
||||
}}
|
||||
onClose={onClose}
|
||||
open={open}
|
||||
>
|
||||
<NavigationMenu/>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
74
front/src/components/navigation/NavItem.js
Normal file
74
front/src/components/navigation/NavItem.js
Normal file
@@ -0,0 +1,74 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import {isNavItemActive} from "./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>
|
||||
);
|
||||
}
|
||||
54
front/src/components/navigation/NavigationMenu.js
Normal file
54
front/src/components/navigation/NavigationMenu.js
Normal file
@@ -0,0 +1,54 @@
|
||||
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>
|
||||
{renderNavItems({items: items, pathname: pathname})}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export function NavigationMenu() {
|
||||
const location = useLocation();
|
||||
const pathname = location.pathname;
|
||||
const {user} = useUser();
|
||||
const [items, setItems] = useState(null)
|
||||
|
||||
const userChild = navItems.filter((item) => !item.forBarmen && !item.forAdmin)
|
||||
const barmenChild = navItems.filter((item) => item.forBarmen)
|
||||
const adminChild = navItems.filter((item) => item.forAdmin)
|
||||
|
||||
useEffect(() => {
|
||||
const role = !user ? "USER" : Object.keys(user).length === 0 ? "USER" : user.role
|
||||
const newState = (
|
||||
<Box component="nav" sx={{flex: '1 1 auto', p: '12px'}}>
|
||||
{renderNavItems({items: userChild, pathname: pathname})}
|
||||
{role !== "USER" && renderSpecialItems(barmenChild, "Для бармена:", pathname)}
|
||||
{role === "ADMIN" && renderSpecialItems(adminChild, "Для админа", pathname)}
|
||||
</Box>
|
||||
)
|
||||
setItems(newState)
|
||||
// eslint-disable-next-line
|
||||
}, [user, pathname]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/*верхняя стопка*/}
|
||||
<Stack spacing={2} sx={{p: 2, height: '63px'}}>
|
||||
<ThemeSwitch/>
|
||||
</Stack>
|
||||
<Divider sx={{borderColor: 'var(--mui-palette-neutral-700)'}}/>
|
||||
{/*меню навигации*/}
|
||||
{items}
|
||||
</>
|
||||
)
|
||||
}
|
||||
37
front/src/components/navigation/SideNav.js
Normal file
37
front/src/components/navigation/SideNav.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import * as React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import {NavigationMenu} from "./NavigationMenu";
|
||||
|
||||
export function SideNav() {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
'--SideNav-background': 'var(--mui-palette-neutral-950)',
|
||||
'--SideNav-color': 'var(--mui-palette-common-white)',
|
||||
'--NavItem-color': 'var(--mui-palette-neutral-300)',
|
||||
'--NavItem-hover-background': 'rgba(255, 255, 255, 0.04)',
|
||||
'--NavItem-active-background': 'var(--mui-palette-primary-main)',
|
||||
'--NavItem-active-color': 'var(--mui-palette-primary-contrastText)',
|
||||
'--NavItem-disabled-color': 'var(--mui-palette-neutral-500)',
|
||||
'--NavItem-icon-color': 'var(--mui-palette-neutral-400)',
|
||||
'--NavItem-icon-active-color': 'var(--mui-palette-primary-contrastText)',
|
||||
'--NavItem-icon-disabled-color': 'var(--mui-palette-neutral-600)',
|
||||
bgcolor: 'var(--SideNav-background)',
|
||||
color: 'var(--SideNav-color)',
|
||||
display: {xs: 'none', xl: 'flex'},
|
||||
flexDirection: 'column',
|
||||
height: '100%',
|
||||
left: 0,
|
||||
maxWidth: '100%',
|
||||
position: 'fixed',
|
||||
scrollbarWidth: 'none',
|
||||
top: 0,
|
||||
width: 'var(--SideNav-width)',
|
||||
zIndex: 'var(--SideNav-zIndex)',
|
||||
'&::-webkit-scrollbar': {display: 'none'},
|
||||
}}
|
||||
>
|
||||
<NavigationMenu/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
25
front/src/components/navigation/isNavItemActive.js
Normal file
25
front/src/components/navigation/isNavItemActive.js
Normal file
@@ -0,0 +1,25 @@
|
||||
export function isNavItemActive({
|
||||
disabled,
|
||||
external,
|
||||
href,
|
||||
matcher,
|
||||
pathname,
|
||||
}) {
|
||||
if (disabled || !href || external) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (matcher) {
|
||||
if (matcher.type === 'startsWith') {
|
||||
return pathname.startsWith(matcher.href);
|
||||
}
|
||||
|
||||
if (matcher.type === 'equals') {
|
||||
return pathname === matcher.href;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return pathname === href;
|
||||
}
|
||||
49
front/src/context/AuthContext.js
Normal file
49
front/src/context/AuthContext.js
Normal file
@@ -0,0 +1,49 @@
|
||||
import * as React from 'react';
|
||||
import {useCallback, useEffect} from 'react';
|
||||
import {logger} from "../lib/DefaultLogger";
|
||||
import {tokenUtil} from "../lib/clients/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;
|
||||
61
front/src/context/SelectContext.js
Normal file
61
front/src/context/SelectContext.js
Normal file
@@ -0,0 +1,61 @@
|
||||
import * as React from "react";
|
||||
|
||||
export const SelectContext = React.createContext(undefined);
|
||||
|
||||
export function SelectProvider({children}) {
|
||||
const [selected, setSelected] = React.useState({
|
||||
cocktail: null,
|
||||
ingredient: null
|
||||
});
|
||||
|
||||
const selectCocktail = (row) => {
|
||||
setSelected((prev) => ({
|
||||
...prev,
|
||||
cocktail: row
|
||||
}))
|
||||
}
|
||||
const getCocktail = () => {
|
||||
return selected.cocktail
|
||||
}
|
||||
const getOpenCocktail = () => {
|
||||
return selected.cocktail !== null;
|
||||
}
|
||||
const closeCocktail = () => {
|
||||
setSelected((prevState) => ({
|
||||
...prevState,
|
||||
cocktail: null,
|
||||
}))
|
||||
}
|
||||
|
||||
const selectIngredient = (row) => {
|
||||
setSelected((prev) => ({
|
||||
...prev,
|
||||
ingredient: row
|
||||
}))
|
||||
}
|
||||
const closeIngredient = () => {
|
||||
setSelected((prevState) => ({
|
||||
...prevState,
|
||||
ingredient: null
|
||||
}))
|
||||
}
|
||||
const getIngredient = () => {
|
||||
return selected.ingredient
|
||||
}
|
||||
const getOpenIngredient = () => {
|
||||
return selected.ingredient !== null
|
||||
}
|
||||
|
||||
return <SelectContext.Provider value={{...selected,
|
||||
selectCocktail,
|
||||
getCocktail,
|
||||
getOpenCocktail,
|
||||
closeCocktail,
|
||||
selectIngredient,
|
||||
closeIngredient,
|
||||
getIngredient,
|
||||
getOpenIngredient
|
||||
}}>{children}</SelectContext.Provider>;
|
||||
}
|
||||
|
||||
export const SelectConsumer = SelectContext.Consumer;
|
||||
55
front/src/context/UserContext.js
Normal file
55
front/src/context/UserContext.js
Normal file
@@ -0,0 +1,55 @@
|
||||
import * as React from "react";
|
||||
import {createContext, useCallback, useEffect, useState} from "react";
|
||||
import {logger} from "../lib/DefaultLogger";
|
||||
import {userClient} from "../lib/clients/UserClient";
|
||||
import {tokenUtil} from "../lib/clients/TokenUtil";
|
||||
|
||||
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 (!tokenUtil.checkToken(tokenUtil.getToken())) {
|
||||
setState((prev) => ({...prev, error: '', isLoading: false, user: {}}));
|
||||
return;
|
||||
}
|
||||
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;
|
||||
|
||||
36
front/src/hooks/useAlert.js
Normal file
36
front/src/hooks/useAlert.js
Normal file
@@ -0,0 +1,36 @@
|
||||
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 createWarning(message) {
|
||||
createAlert(message, {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, createWarning}
|
||||
}
|
||||
14
front/src/hooks/useAuth.js
Normal file
14
front/src/hooks/useAuth.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import * as React from 'react';
|
||||
import {AuthContext} from "../context/AuthContext";
|
||||
|
||||
export function useAuth() {
|
||||
const context = React.useContext(AuthContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error('useAuth must be used within a AuthProvider');
|
||||
}
|
||||
|
||||
window.auth = context;
|
||||
|
||||
return context;
|
||||
}
|
||||
20
front/src/hooks/usePopover.js
Normal file
20
front/src/hooks/usePopover.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import * as React from 'react';
|
||||
|
||||
export function usePopover() {
|
||||
const anchorRef = React.useRef(null);
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
const handleOpen = React.useCallback(() => {
|
||||
setOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleClose = React.useCallback(() => {
|
||||
setOpen(false);
|
||||
}, []);
|
||||
|
||||
const handleToggle = React.useCallback(() => {
|
||||
setOpen((prevState) => !prevState);
|
||||
}, []);
|
||||
|
||||
return { anchorRef, handleClose, handleOpen, handleToggle, open };
|
||||
}
|
||||
14
front/src/hooks/useSelect.js
Normal file
14
front/src/hooks/useSelect.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import * as React from "react";
|
||||
import {SelectContext} from "../context/SelectContext";
|
||||
|
||||
export function useSelect() {
|
||||
const context = React.useContext(SelectContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error('useSelect must be used within a SelectProvider');
|
||||
}
|
||||
|
||||
window.select = context;
|
||||
|
||||
return context;
|
||||
}
|
||||
47
front/src/hooks/useSelection.js
Normal file
47
front/src/hooks/useSelection.js
Normal file
@@ -0,0 +1,47 @@
|
||||
import * as React from 'react';
|
||||
|
||||
// IMPORTANT: To prevent infinite loop, `keys` argument must be memoized with React.useMemo hook.
|
||||
export function useSelection(keys) {
|
||||
const [selected, setSelected] = React.useState(new Set());
|
||||
|
||||
React.useEffect(() => {
|
||||
setSelected(new Set());
|
||||
}, [keys]);
|
||||
|
||||
const handleDeselectAll = React.useCallback(() => {
|
||||
setSelected(new Set());
|
||||
}, []);
|
||||
|
||||
const handleDeselectOne = React.useCallback((key) => {
|
||||
setSelected((prev) => {
|
||||
const copy = new Set(prev);
|
||||
copy.delete(key);
|
||||
return copy;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleSelectAll = React.useCallback(() => {
|
||||
setSelected(new Set(keys));
|
||||
}, [keys]);
|
||||
|
||||
const handleSelectOne = React.useCallback((key) => {
|
||||
setSelected((prev) => {
|
||||
const copy = new Set(prev);
|
||||
copy.add(key);
|
||||
return copy;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const selectedAny = selected.size > 0;
|
||||
const selectedAll = selected.size === keys.length;
|
||||
|
||||
return {
|
||||
deselectAll: handleDeselectAll,
|
||||
deselectOne: handleDeselectOne,
|
||||
selectAll: handleSelectAll,
|
||||
selectOne: handleSelectOne,
|
||||
selected,
|
||||
selectedAny,
|
||||
selectedAll,
|
||||
};
|
||||
}
|
||||
13
front/src/hooks/useUser.js
Normal file
13
front/src/hooks/useUser.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import * as React from "react";
|
||||
import {UserContext} from "../context/UserContext";
|
||||
|
||||
export function useUser() {
|
||||
const context = React.useContext(UserContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error('useUser must be used within a UserProvider');
|
||||
}
|
||||
|
||||
window.user = context;
|
||||
return context;
|
||||
}
|
||||
13
front/src/index.css
Normal file
13
front/src/index.css
Normal file
@@ -0,0 +1,13 @@
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
13
front/src/index.js
Normal file
13
front/src/index.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './app/App';
|
||||
import './index.css';
|
||||
import '@fontsource/roboto/300.css';
|
||||
import '@fontsource/roboto/400.css';
|
||||
import '@fontsource/roboto/500.css';
|
||||
import '@fontsource/roboto/700.css';
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
root.render(
|
||||
<App/>
|
||||
);
|
||||
4
front/src/lib/DefaultLogger.js
Normal file
4
front/src/lib/DefaultLogger.js
Normal file
@@ -0,0 +1,4 @@
|
||||
import {createLogger} from "./Logger";
|
||||
import {config} from "../Config";
|
||||
|
||||
export const logger = createLogger({ level: config.logLevel });
|
||||
65
front/src/lib/Logger.js
Normal file
65
front/src/lib/Logger.js
Normal file
@@ -0,0 +1,65 @@
|
||||
/* eslint-disable no-console -- Allow */
|
||||
|
||||
// NOTE: A tracking system such as Sentry should replace the console
|
||||
|
||||
export const LogLevel = {NONE: 'NONE', ERROR: 'ERROR', WARN: 'WARN', DEBUG: 'DEBUG', ALL: 'ALL'};
|
||||
|
||||
const LogLevelNumber = {NONE: 0, ERROR: 1, WARN: 2, DEBUG: 3, ALL: 4};
|
||||
|
||||
export class Logger {
|
||||
prefix;
|
||||
level;
|
||||
showLevel;
|
||||
|
||||
levelNumber;
|
||||
|
||||
constructor({prefix = '', level = LogLevel.ALL, showLevel = true}) {
|
||||
this.prefix = prefix;
|
||||
this.level = level;
|
||||
this.levelNumber = LogLevelNumber[this.level];
|
||||
this.showLevel = showLevel;
|
||||
}
|
||||
|
||||
debug = (...args) => {
|
||||
if (this.canWrite(LogLevel.DEBUG)) {
|
||||
this.write(LogLevel.DEBUG, ...args);
|
||||
}
|
||||
};
|
||||
|
||||
warn = (...args) => {
|
||||
if (this.canWrite(LogLevel.WARN)) {
|
||||
this.write(LogLevel.WARN, ...args);
|
||||
}
|
||||
};
|
||||
|
||||
error = (...args) => {
|
||||
if (this.canWrite(LogLevel.ERROR)) {
|
||||
this.write(LogLevel.ERROR, ...args);
|
||||
}
|
||||
};
|
||||
|
||||
canWrite(level) {
|
||||
return this.levelNumber >= LogLevelNumber[level];
|
||||
}
|
||||
|
||||
write(level, ...args) {
|
||||
let prefix = this.prefix;
|
||||
|
||||
if (this.showLevel) {
|
||||
prefix = `- ${level} ${prefix}`;
|
||||
}
|
||||
|
||||
if (level === LogLevel.ERROR) {
|
||||
console.error(prefix, ...args);
|
||||
} else {
|
||||
console.log(prefix, ...args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This can be extended to create context specific logger (Server Action, Router Handler, etc.)
|
||||
// to add context information (IP, User-Agent, timestamp, etc.)
|
||||
|
||||
export function createLogger({prefix, level} = {}) {
|
||||
return new Logger({prefix, level});
|
||||
}
|
||||
45
front/src/lib/clients/AuthClient.js
Normal file
45
front/src/lib/clients/AuthClient.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import {api} from "./api";
|
||||
import {requests} from "../../requests";
|
||||
|
||||
class AuthClient {
|
||||
|
||||
async signOut() {
|
||||
localStorage.removeItem("token");
|
||||
return {};
|
||||
}
|
||||
|
||||
async login(request, setLoading, setError, checkSession) {
|
||||
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();
|
||||
}
|
||||
|
||||
loginByCode(code, checkSession) {
|
||||
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();
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const authClient = new AuthClient();
|
||||
74
front/src/lib/clients/BarClient.js
Normal file
74
front/src/lib/clients/BarClient.js
Normal file
@@ -0,0 +1,74 @@
|
||||
import {api} from "./api";
|
||||
import {requests} from "../../requests";
|
||||
import {getComparator} from "../../components/core/getComparator";
|
||||
|
||||
class BarClient {
|
||||
|
||||
getBarList(setBars, createError) {
|
||||
api().get(requests.bar.all)
|
||||
.then((r) => {
|
||||
setBars(r.data.sort(getComparator("name")))
|
||||
})
|
||||
.catch(() => {
|
||||
createError("Ошибка получения списков")
|
||||
})
|
||||
}
|
||||
|
||||
changeBar(id, bars, createWarning, createSuccess, createError, setBars) {
|
||||
createWarning("Дождитесь окончания операции")
|
||||
api().post(`${requests.bar.change}/${id}`)
|
||||
.then(() => createSuccess("Список изменен"))
|
||||
.catch(() => createError("Ошибка изменения активного списка"))
|
||||
|
||||
const newState = bars.map((b) => {
|
||||
if (b.active) {
|
||||
return {
|
||||
...b, active: false
|
||||
}
|
||||
}
|
||||
if (b.id === id) {
|
||||
return {
|
||||
...b, active: true
|
||||
}
|
||||
}
|
||||
return b;
|
||||
})
|
||||
setBars(newState);
|
||||
}
|
||||
|
||||
deleteBar(bar, bars, createError, createSuccess, setBars) {
|
||||
if (bar.active) {
|
||||
createError("Нельзя удалить активный бар!")
|
||||
return;
|
||||
}
|
||||
api().delete(requests.bar.crud + bar.id)
|
||||
.then(() => createSuccess("Список удален"))
|
||||
.catch(() => createError("Ошибка удаления. Обновите страницу"))
|
||||
|
||||
setBars(bars.filter((b) => b.id !== bar.id));
|
||||
}
|
||||
|
||||
createBar(name, bars, createSuccess, createError, setBars, setOpen) {
|
||||
api().post(requests.bar.crud + name)
|
||||
.then((r) => {
|
||||
createSuccess("Cписок создан");
|
||||
let state = bars;
|
||||
state.push(r.data);
|
||||
setBars(state)
|
||||
setOpen(false)
|
||||
}).catch(() => createError("Ошибка создания списка"))
|
||||
}
|
||||
|
||||
copyBar(oldId, newName, setBars, bars, createError, createSuccess, setOpen) {
|
||||
api().post(requests.bar.crud + "copy/" + oldId + "/" + newName)
|
||||
.then((r) => {
|
||||
const state = bars;
|
||||
state.push(r.data)
|
||||
setBars(state);
|
||||
createSuccess("Бар скопирован")
|
||||
setOpen(false)
|
||||
}).catch(() => createError("Ошибка при копировании бара"))
|
||||
}
|
||||
}
|
||||
|
||||
export const barClient = new BarClient();
|
||||
19
front/src/lib/clients/CategoryClient.js
Normal file
19
front/src/lib/clients/CategoryClient.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import {api} from "./api";
|
||||
import {requests} from "../../requests";
|
||||
import {getComparator} from "../../components/core/getComparator";
|
||||
|
||||
class CategoryClient {
|
||||
|
||||
getCategoryList(setCategory, createError) {
|
||||
api().get(requests.category.basic)
|
||||
.then((r) => {
|
||||
setCategory(r.data.sort(getComparator())
|
||||
.map((item, i) => {
|
||||
return {id: i, name: item}
|
||||
}))
|
||||
})
|
||||
.catch(() => createError("Ошибка получения категорий"))
|
||||
}
|
||||
};
|
||||
|
||||
export const categoryClient = new CategoryClient();
|
||||
216
front/src/lib/clients/CocktailClient.js
Normal file
216
front/src/lib/clients/CocktailClient.js
Normal file
@@ -0,0 +1,216 @@
|
||||
import {api} from "./api";
|
||||
import {requests} from "../../requests";
|
||||
import {sortList} from "../../components/cocktails/sortingList";
|
||||
import {getComparator} from "../../components/core/getComparator";
|
||||
|
||||
class CocktailClient {
|
||||
emptyCocktailForEditPage = {
|
||||
id: null,
|
||||
name: "",
|
||||
alcoholic: "",
|
||||
category: "",
|
||||
components: "",
|
||||
glass: "",
|
||||
image: "",
|
||||
instructions: "",
|
||||
isAllowed: false,
|
||||
rating: {
|
||||
rating: 0,
|
||||
favourite: false
|
||||
},
|
||||
receipt: [],
|
||||
tags: "",
|
||||
video: ""
|
||||
}
|
||||
|
||||
getMenu(setRows, setIsNew, setPage, setLoad, setIsEnd, isNew, rows, page, size, filter, createError) {
|
||||
setLoad(true);
|
||||
|
||||
const request = {
|
||||
...filter,
|
||||
sort: sortList.find((s) => s.name === filter.sorting).id,
|
||||
page: page + 1,
|
||||
size: size,
|
||||
notHaveCount: 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.code)
|
||||
})
|
||||
}
|
||||
|
||||
async getCocktailByIngredient(ingredient, setCocktails) {
|
||||
if(!ingredient) {
|
||||
return
|
||||
}
|
||||
api().get(requests.cocktails.byIngredient + ingredient.id)
|
||||
.then((r) => setCocktails(r.data))
|
||||
}
|
||||
|
||||
getCocktailsForCalcPage(load,setLoad, setCocktails, setCocktailMap, createError) {
|
||||
if (load) {
|
||||
return;
|
||||
}
|
||||
api().get(requests.cocktails.calc)
|
||||
.then((r) => {
|
||||
const data = r.data;
|
||||
if (data.length === 0) {
|
||||
setLoad(false);
|
||||
return;
|
||||
}
|
||||
setCocktails(data);
|
||||
let map = {};
|
||||
data.forEach((d) => {
|
||||
map = {
|
||||
...map,
|
||||
[d.id]: 1
|
||||
}
|
||||
})
|
||||
setCocktailMap(map);
|
||||
setLoad(true);
|
||||
})
|
||||
.catch((r) => {
|
||||
setLoad(true);
|
||||
createError("Ошибка загрузки данных от сервера Status:" + r.code)
|
||||
})
|
||||
}
|
||||
|
||||
savePhoto(event, changeCocktailValue, getError) {
|
||||
const file = event.target.files[0];
|
||||
let formData = new FormData();
|
||||
formData.append('file', file);
|
||||
api().post(requests.cocktails.photo, formData)
|
||||
.then((r) => changeCocktailValue("image", r.data))
|
||||
.catch(() => getError())
|
||||
}
|
||||
|
||||
deleteCocktailFromEdit(setCocktails, setCocktail, createError, cocktails, cocktail, emptyCocktail) {
|
||||
api().delete(requests.cocktails.cocktail + cocktail.id)
|
||||
.then(() => {
|
||||
setCocktails(cocktails.filter((r) => r.id !== cocktail.id))
|
||||
setCocktail(emptyCocktail);
|
||||
})
|
||||
.catch(() => createError("Ошибка удаления коктейля"))
|
||||
}
|
||||
|
||||
deleteCocktail(id, rows, setRows, createSuccess, createError) {
|
||||
api().delete(requests.cocktails.cocktail + id)
|
||||
.then(() => {
|
||||
setRows(rows.filter((r) => r.id !== id))
|
||||
createSuccess("Коктейль удален")
|
||||
})
|
||||
.catch(() => createError("Ошибка удаления коктейля"))
|
||||
}
|
||||
|
||||
async hiddenCocktail(id) {
|
||||
return api().post(requests.cocktails.hide + id);
|
||||
}
|
||||
|
||||
saveChangeCocktail (cocktail, createError, createSuccess) {
|
||||
api().patch(requests.cocktails.basic, cocktail)
|
||||
.then((r) => {
|
||||
if (!r.data.error) {
|
||||
createSuccess("Сохранено")
|
||||
return;
|
||||
}
|
||||
createError("Ошибка на сервере: " + r.data.error)
|
||||
})
|
||||
.catch(() => createError("Неизвестная ошибка"))
|
||||
}
|
||||
|
||||
getOneCocktail (selected, setCocktail, getError, emptyCocktail) {
|
||||
if (!selected) {
|
||||
setCocktail(emptyCocktail);
|
||||
return;
|
||||
}
|
||||
api().get(requests.cocktails.cocktail + selected)
|
||||
.then((r) => {
|
||||
setCocktail(r.data)
|
||||
})
|
||||
.catch(() => getError());
|
||||
}
|
||||
|
||||
getSimpleList(setCocktails, setSelected, setLoading, createError, currentId) {
|
||||
api().get(requests.cocktails.simple)
|
||||
.then((r) => {
|
||||
const arr = r.data.sort(getComparator("asc", "name"));
|
||||
setCocktails(arr)
|
||||
|
||||
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("Ошибка получения данных"))
|
||||
}
|
||||
|
||||
getCocktailForModal (row, setLoading, setCocktail, closeCocktail, getError) {
|
||||
setLoading(true)
|
||||
if (!row) {
|
||||
setLoading(false)
|
||||
return;
|
||||
}
|
||||
api().get(requests.cocktails.modal + row)
|
||||
.then((r) => {
|
||||
setCocktail(r.data)
|
||||
setLoading(false)
|
||||
})
|
||||
.catch(() => {
|
||||
getError();
|
||||
setLoading(false)
|
||||
closeCocktail();
|
||||
})
|
||||
}
|
||||
|
||||
changeFavourite(value, id, newState, setRows, createSuccess, createError) {
|
||||
const url = `${requests.cocktails.favourite}${id}`;
|
||||
const request = value ? api().put(url) : api().delete(url);
|
||||
|
||||
request
|
||||
.then(() => {
|
||||
setRows(newState);
|
||||
createSuccess("Спасибо за оценку!")
|
||||
}).catch(() => createError("Ошибка сохранения"))
|
||||
}
|
||||
|
||||
changeRating(id, newState, value, setRows, createSuccess, createError) {
|
||||
api().post(`${requests.cocktails.rating}${id}&rating=${value}`)
|
||||
.then(() => {
|
||||
setRows(newState);
|
||||
createSuccess("Спасибо за оценку!")
|
||||
}).catch(() => createError("Ошибка сохранения"))
|
||||
}
|
||||
|
||||
drinkCocktail(id, createSuccess, createError) {
|
||||
api().post(`${requests.cocktails.drink}/${id}`)
|
||||
.then(() => createSuccess("Бон аппетит"))
|
||||
.catch(() => createError("Ошибка отметки коктейля"))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const cocktailClient = new CocktailClient();
|
||||
17
front/src/lib/clients/GlassClient.js
Normal file
17
front/src/lib/clients/GlassClient.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import {api} from "./api";
|
||||
import {requests} from "../../requests";
|
||||
import {getComparator} from "../../components/core/getComparator";
|
||||
|
||||
class GlassClient {
|
||||
|
||||
getGlassList(setGlass, createError) {
|
||||
api().get(requests.glass.list)
|
||||
.then((r) => setGlass(r.data.sort(getComparator())
|
||||
.map((item, i) => {
|
||||
return {id: i, name: item}
|
||||
})))
|
||||
.catch(() => createError("Ошибка получения посуды"))
|
||||
}
|
||||
}
|
||||
|
||||
export const glassClient = new GlassClient();
|
||||
112
front/src/lib/clients/IngredientClient.js
Normal file
112
front/src/lib/clients/IngredientClient.js
Normal file
@@ -0,0 +1,112 @@
|
||||
import {api} from "./api";
|
||||
import {requests} from "../../requests";
|
||||
import {getComparator} from "../../components/core/getComparator";
|
||||
|
||||
class IngredientClient {
|
||||
|
||||
allList(currentId, setIngredients, setIngredient, createError) {
|
||||
api().get(requests.ingredient.all)
|
||||
.then((r) => {
|
||||
const arr = r.data.sort(getComparator("asc", "name"));
|
||||
setIngredients(arr)
|
||||
|
||||
if (!currentId) {
|
||||
return;
|
||||
}
|
||||
const currentIngredient = arr.find((r) => r.id === (currentId * 1));
|
||||
if (!currentIngredient) {
|
||||
return;
|
||||
}
|
||||
setIngredient(currentIngredient);
|
||||
})
|
||||
.catch(() => createError("Ошибка получения данных"))
|
||||
}
|
||||
|
||||
getType(setTypes) {
|
||||
api().get(requests.ingredient.type)
|
||||
.then((r) => setTypes(r.data.sort(getComparator("asc", "name"))))
|
||||
}
|
||||
|
||||
findAll(setIngredients, createError) {
|
||||
api().get(requests.ingredient.all)
|
||||
.then((r) => setIngredients(r.data.sort(getComparator("asc", "name"))))
|
||||
.catch(() => createError("Ошибка получения списка ингредиентов"))
|
||||
}
|
||||
|
||||
getAllIngredients(setIngredients, setLoading, createError) {
|
||||
api().get(requests.ingredient.all)
|
||||
.then((r) => {
|
||||
setIngredients(r.data)
|
||||
setLoading(false);
|
||||
})
|
||||
.catch(() => {
|
||||
createError("Ошибка получения списка ингредиентов");
|
||||
setLoading(false);
|
||||
})
|
||||
}
|
||||
|
||||
changeIngredientIsHave(id, value, state, setIngredients, createError) {
|
||||
const url = `${requests.ingredient.crud}?id=${id}`;
|
||||
const request = value ? api().put(url) : api().delete(url);
|
||||
request
|
||||
.then(() => {
|
||||
setIngredients(state);
|
||||
})
|
||||
.catch(() => {
|
||||
createError("Ошибка изменения ингредиента");
|
||||
});
|
||||
}
|
||||
|
||||
deleteIngredientIsHave(id, createSuccess, createError) {
|
||||
api().delete(`${requests.ingredient.crud}/${id}`)
|
||||
.then(() => createSuccess("Ингредиент удален"))
|
||||
.catch(() => createError("Ошибка удаления ингредиента. Перезагрузите страницу"))
|
||||
}
|
||||
|
||||
saveIngredient(ingredient, createSuccess, createError) {
|
||||
api().patch(requests.ingredient.crud, ingredient)
|
||||
.then(() => createSuccess("Ингредиент сохранен"))
|
||||
.catch(() => createError("Ошибка сохранения"))
|
||||
}
|
||||
|
||||
findOne(id, selectIngredient, createError) {
|
||||
api().get(`${requests.bar.ingredient}?id=${id}`)
|
||||
.then((r) => {
|
||||
selectIngredient(r.data)
|
||||
})
|
||||
.catch(() => createError("Ошибка получения информации об ингредиенте"))
|
||||
}
|
||||
|
||||
changeIngredientInBar(ingredient, cocktail, setCocktail, createSuccess, createError) {
|
||||
const url = `${requests.ingredient.crud}?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("Ошибка сохранения"))
|
||||
}
|
||||
|
||||
findUnit(setUnits, createError) {
|
||||
api().get(requests.unit)
|
||||
.then((r) => setUnits(r.data.sort(getComparator("asc", "name"))))
|
||||
.catch(() => createError("Ошибка получения единиц измерения"))
|
||||
}
|
||||
}
|
||||
|
||||
export const ingredientClient = new IngredientClient()
|
||||
34
front/src/lib/clients/TokenUtil.js
Normal file
34
front/src/lib/clients/TokenUtil.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import {decodeToken, isExpired} from "react-jwt";
|
||||
import {requests} from "../../requests";
|
||||
import axios from "axios";
|
||||
|
||||
class TokenUtil {
|
||||
|
||||
checkToken(token) {
|
||||
if (token == null || isExpired(token)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.refreshToken();
|
||||
return true;
|
||||
}
|
||||
|
||||
getToken() {
|
||||
return localStorage.getItem("token");
|
||||
}
|
||||
|
||||
refreshToken() {
|
||||
const decoded = decodeToken(this.getToken());
|
||||
const currentTime = Date.now() / 1000;
|
||||
if (decoded.exp - currentTime > 43200) {
|
||||
return
|
||||
}
|
||||
|
||||
axios.post(requests.auth.refresh, {}, {headers: {'Authorization': this.getToken()}})
|
||||
.then((r) => {
|
||||
localStorage.setItem("token", r.data.token)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const tokenUtil = new TokenUtil();
|
||||
17
front/src/lib/clients/UserClient.js
Normal file
17
front/src/lib/clients/UserClient.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import {requests} from "../../requests";
|
||||
import {api} from "./api";
|
||||
|
||||
class UserClient {
|
||||
|
||||
async getMe() {
|
||||
try{
|
||||
let url = requests.auth.getMe
|
||||
const response = await api().get(url);
|
||||
return {data: response.data}
|
||||
} catch (e) {
|
||||
return {errorData: e.data}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const userClient = new UserClient();
|
||||
16
front/src/lib/clients/api.js
Normal file
16
front/src/lib/clients/api.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import axios from "axios";
|
||||
import {tokenUtil} from "./TokenUtil";
|
||||
|
||||
const host = "localhost:8080"; //дебаг вместе с беком
|
||||
// const host = "192.168.1.100:8091"; //дебаг фронта
|
||||
// const host = "bar.kayashov.keenetic.pro"; //прод
|
||||
export const api = () => {
|
||||
const result = axios;
|
||||
result.defaults.baseURL = `${window.location.protocol}//${host}/`;
|
||||
if (tokenUtil.checkToken(tokenUtil.getToken())) {
|
||||
result.defaults.headers.common["Authorization"] = "Bearer " + tokenUtil.getToken();
|
||||
} else {
|
||||
delete result.defaults.headers.common
|
||||
}
|
||||
return result;
|
||||
}
|
||||
11
front/src/lib/getSiteUrl.js
Normal file
11
front/src/lib/getSiteUrl.js
Normal file
@@ -0,0 +1,11 @@
|
||||
export function getSiteURL() {
|
||||
let url =
|
||||
process.env.NEXT_PUBLIC_SITE_URL ?? // Set this to your site URL in production env.
|
||||
process.env.NEXT_PUBLIC_VERCEL_URL ?? // Automatically set by Vercel.
|
||||
'http://localhost:3000/';
|
||||
// Make sure to include `https://` when not localhost.
|
||||
url = url.includes('http') ? url : `https://${url}`;
|
||||
// Make sure to include a trailing `/`.
|
||||
url = url.endsWith('/') ? url : `${url}/`;
|
||||
return url;
|
||||
}
|
||||
23
front/src/logo.svg
Normal file
23
front/src/logo.svg
Normal file
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg fill="#ffffff" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 0 512 512" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<rect x="169.69" y="496.792" width="231.799" height="15.208"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M293.189,297.798L471.835,95.29H245.669C241.764,42.091,197.236,0,143.058,0C86.323,0,40.165,46.158,40.165,102.894
|
||||
c0,56.735,46.157,102.891,102.893,102.891c15.495,0,30.831-3.527,44.722-10.236l90.201,102.25v168.576h-77.876v15.208h170.958
|
||||
v-15.208h-77.875V297.798z M230.415,95.288h-69l48.752-48.751C221.462,59.967,228.82,76.812,230.415,95.288z M150.662,15.552
|
||||
c18.474,1.595,35.331,8.928,48.76,20.222l-48.76,48.76V15.552z M135.454,15.542v68.932L86.701,35.775
|
||||
C100.131,24.481,116.981,17.135,135.454,15.542z M75.947,46.528l48.812,48.76H55.708C57.302,76.811,64.65,59.958,75.947,46.528z
|
||||
M55.706,110.496h57.044l5.601,6.35l-42.408,42.408C64.647,145.824,57.3,128.97,55.706,110.496z M135.454,190.244
|
||||
c-18.475-1.594-35.328-8.94-48.758-20.237l41.735-41.735l7.023,7.961V190.244z M150.662,190.224v-36.751l26.603,30.156
|
||||
C168.817,187.207,159.823,189.424,150.662,190.224z M201.485,188.096l-21.039-23.848h132.009V149.04H167.031l-33.999-38.541
|
||||
h112.92h192.188l-34,38.54h-64.81v15.208h51.395L285.586,283.43L201.485,188.096z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
10
front/src/navItems.js
Normal file
10
front/src/navItems.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import {paths} from "./path";
|
||||
|
||||
export const navItems = [
|
||||
{key: 'menu', title: 'Меню', href: paths.dashboard.overview, icon: 'menu'},
|
||||
{key: 'barList', title: 'Список баров', href: paths.bar.list, icon: 'basket', forBarmen: true},
|
||||
{key: 'ingredients', title: 'Список ингредиентов', href: paths.bar.ingredients, icon: 'basket', forBarmen: true},
|
||||
{key: 'ingredientEdit', title: 'Ингредиенты', href: paths.bar.ingredientEdit, icon: 'ingredients', forAdmin: true},
|
||||
{key: 'cocktailEdit', title: 'Коктейли', href: paths.bar.cocktailEdit, icon: 'cocktail', forAdmin: true},
|
||||
{key: 'calc', title: 'Калькулятор', href: paths.bar.calc, icon: 'calc', forAdmin: true},
|
||||
];
|
||||
25
front/src/path.js
Normal file
25
front/src/path.js
Normal file
@@ -0,0 +1,25 @@
|
||||
export const paths = {
|
||||
home: '/',
|
||||
auth: {signIn: '/auth/sign-in', bot: 'https://t.me/kayashovBarClientBot', tg: '/tg'},
|
||||
dashboard: {
|
||||
overview: '/menu'
|
||||
},
|
||||
visitor: {
|
||||
inBar: "/visitors"
|
||||
},
|
||||
orders: {
|
||||
my: '/orders'
|
||||
},
|
||||
bar: {
|
||||
list: "/barList",
|
||||
ordersQueue: '/queue',
|
||||
ingredients: '/ingredients',
|
||||
cocktails: "/cocktails",
|
||||
ingredientEdit: '/ingredients/edit',
|
||||
cocktailEdit: '/cocktail/edit',
|
||||
menu: '/menuList',
|
||||
calc: '/calc',
|
||||
},
|
||||
errors: {notFound: '/errors/not-found'},
|
||||
notFound: '*',
|
||||
};
|
||||
53
front/src/requests.js
Normal file
53
front/src/requests.js
Normal file
@@ -0,0 +1,53 @@
|
||||
const host = "api/";
|
||||
|
||||
const routes = {
|
||||
auth: host + "auth/",
|
||||
bar: host + "bar/",
|
||||
category: host + "category",
|
||||
cocktail: host + "cocktail",
|
||||
glass: host + "glass",
|
||||
ingredient: host + "ingredient",
|
||||
receipt: host + "receipt",
|
||||
unit: host + "unit",
|
||||
}
|
||||
|
||||
export const requests = {
|
||||
auth: {
|
||||
login: routes.auth + "login",
|
||||
refresh: routes.auth + "refresh",
|
||||
singOut: "signOut",
|
||||
getMe: routes.auth + "getMe",
|
||||
},
|
||||
bar: {
|
||||
crud: routes.bar,
|
||||
all: routes.bar + "all",
|
||||
change: routes.bar + "change",
|
||||
},
|
||||
category: {
|
||||
basic: routes.category,
|
||||
},
|
||||
cocktails: {
|
||||
menu: routes.cocktail + "/menu",
|
||||
calc: routes.cocktail + "/calc",
|
||||
byIngredient: routes.cocktail + "/byIngredient?id=",
|
||||
photo: routes.cocktail + "/photo",
|
||||
cocktail: routes.cocktail + "?id=",
|
||||
basic: routes.cocktail,
|
||||
simple: routes.cocktail + "/simple",
|
||||
modal: routes.cocktail + "/modal?id=",
|
||||
favourite: routes.cocktail + "/favourite?id=",
|
||||
rating: routes.cocktail + "/rating?id=",
|
||||
drink: routes.cocktail + "/drink",
|
||||
hide: routes.cocktail + "/hidden/",
|
||||
},
|
||||
glass: {
|
||||
list: routes.glass,
|
||||
},
|
||||
ingredient: {
|
||||
all: routes.ingredient + "/all",
|
||||
simple: routes.ingredient + "/simple",
|
||||
type: routes.ingredient + "/type",
|
||||
crud: routes.ingredient,
|
||||
},
|
||||
unit: routes.unit + "/units"
|
||||
}
|
||||
33
front/src/styles/global.css
Normal file
33
front/src/styles/global.css
Normal file
@@ -0,0 +1,33 @@
|
||||
/* Remove if fonts are not used */
|
||||
@import '~@fontsource/inter/100.css';
|
||||
@import '~@fontsource/inter/200.css';
|
||||
@import '~@fontsource/inter/300.css';
|
||||
@import '~@fontsource/inter/400.css';
|
||||
@import '~@fontsource/inter/500.css';
|
||||
@import '~@fontsource/inter/600.css';
|
||||
@import '~@fontsource/inter/700.css';
|
||||
@import '~@fontsource/inter/800.css';
|
||||
@import '~@fontsource/inter/900.css';
|
||||
@import '~@fontsource/roboto-mono/300.css';
|
||||
@import '~@fontsource/roboto-mono/400.css';
|
||||
@import '~@fontsource/plus-jakarta-sans/600.css';
|
||||
@import '~@fontsource/plus-jakarta-sans/700.css';
|
||||
|
||||
/* Variables */
|
||||
:root {
|
||||
--icon-fontSize-sm: 1rem;
|
||||
--icon-fontSize-md: 1.25rem;
|
||||
--icon-fontSize-lg: 1.5rem;
|
||||
}
|
||||
|
||||
*:focus-visible {
|
||||
outline: 2px solid var(--mui-palette-primary-main);
|
||||
}
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
height: 100%;
|
||||
}
|
||||
140
front/src/styles/theme/color-schemes.js
Normal file
140
front/src/styles/theme/color-schemes.js
Normal file
@@ -0,0 +1,140 @@
|
||||
import {california, kepple, neonBlue, nevada, redOrange, shakespeare, stormGrey} from './colors';
|
||||
|
||||
export const colorSchemes = {
|
||||
dark: {
|
||||
palette: {
|
||||
action: {disabledBackground: 'rgba(0, 0, 0, 0.12)'},
|
||||
background: {
|
||||
default: 'var(--mui-palette-neutral-950)',
|
||||
defaultChannel: '9 10 11',
|
||||
paper: 'var(--mui-palette-neutral-900)',
|
||||
paperChannel: '19 78 72',
|
||||
level1: 'var(--mui-palette-neutral-800)',
|
||||
level2: 'var(--mui-palette-neutral-700)',
|
||||
level3: 'var(--mui-palette-neutral-600)',
|
||||
},
|
||||
mode: 'dark',
|
||||
common: {black: '#000000', white: '#ffffff'},
|
||||
divider: 'var(--mui-palette-neutral-700)',
|
||||
dividerChannel: '50 56 62',
|
||||
error: {
|
||||
...redOrange,
|
||||
light: redOrange[300],
|
||||
main: redOrange[400],
|
||||
dark: redOrange[500],
|
||||
contrastText: 'var(--mui-palette-common-black)',
|
||||
},
|
||||
info: {
|
||||
...shakespeare,
|
||||
light: shakespeare[300],
|
||||
main: shakespeare[400],
|
||||
dark: shakespeare[500],
|
||||
contrastText: 'var(--mui-palette-common-black)',
|
||||
},
|
||||
neutral: {...nevada},
|
||||
primary: {
|
||||
...neonBlue,
|
||||
light: neonBlue[300],
|
||||
main: neonBlue[400],
|
||||
dark: neonBlue[500],
|
||||
contrastText: 'var(--mui-palette-common-black)',
|
||||
},
|
||||
secondary: {
|
||||
...nevada,
|
||||
light: nevada[100],
|
||||
main: nevada[200],
|
||||
dark: nevada[300],
|
||||
contrastText: 'var(--mui-palette-common-black)',
|
||||
},
|
||||
success: {
|
||||
...kepple,
|
||||
light: kepple[300],
|
||||
main: kepple[400],
|
||||
dark: kepple[500],
|
||||
contrastText: 'var(--mui-palette-common-black)',
|
||||
},
|
||||
text: {
|
||||
primary: 'var(--mui-palette-neutral-100)',
|
||||
primaryChannel: '240 244 248',
|
||||
secondary: 'var(--mui-palette-neutral-400)',
|
||||
secondaryChannel: '159 166 173',
|
||||
disabled: 'var(--mui-palette-neutral-600)',
|
||||
},
|
||||
warning: {
|
||||
...california,
|
||||
light: california[300],
|
||||
main: california[400],
|
||||
dark: california[500],
|
||||
contrastText: 'var(--mui-palette-common-black)',
|
||||
},
|
||||
},
|
||||
},
|
||||
light: {
|
||||
palette: {
|
||||
action: {disabledBackground: 'rgba(0, 0, 0, 0.06)'},
|
||||
background: {
|
||||
default: 'var(--mui-palette-common-white)',
|
||||
defaultChannel: '255 255 255',
|
||||
paper: 'var(--mui-palette-common-white)',
|
||||
paperChannel: '255 255 255',
|
||||
level1: 'var(--mui-palette-neutral-50)',
|
||||
level2: 'var(--mui-palette-neutral-100)',
|
||||
level3: 'var(--mui-palette-neutral-200)',
|
||||
},
|
||||
mode: 'light',
|
||||
common: {black: '#000000', white: '#ffffff'},
|
||||
divider: 'var(--mui-palette-neutral-200)',
|
||||
dividerChannel: '220 223 228',
|
||||
error: {
|
||||
...redOrange,
|
||||
light: redOrange[400],
|
||||
main: redOrange[500],
|
||||
dark: redOrange[600],
|
||||
contrastText: 'var(--mui-palette-common-white)',
|
||||
},
|
||||
info: {
|
||||
...shakespeare,
|
||||
light: shakespeare[400],
|
||||
main: shakespeare[500],
|
||||
dark: shakespeare[600],
|
||||
contrastText: 'var(--mui-palette-common-white)',
|
||||
},
|
||||
neutral: {...stormGrey},
|
||||
primary: {
|
||||
...neonBlue,
|
||||
light: neonBlue[400],
|
||||
main: neonBlue[500],
|
||||
dark: neonBlue[600],
|
||||
contrastText: 'var(--mui-palette-common-white)',
|
||||
},
|
||||
secondary: {
|
||||
...nevada,
|
||||
light: nevada[600],
|
||||
main: nevada[700],
|
||||
dark: nevada[800],
|
||||
contrastText: 'var(--mui-palette-common-white)',
|
||||
},
|
||||
success: {
|
||||
...kepple,
|
||||
light: kepple[400],
|
||||
main: kepple[500],
|
||||
dark: kepple[600],
|
||||
contrastText: 'var(--mui-palette-common-white)',
|
||||
},
|
||||
text: {
|
||||
primary: 'var(--mui-palette-neutral-900)',
|
||||
primaryChannel: '33 38 54',
|
||||
secondary: 'var(--mui-palette-neutral-500)',
|
||||
secondaryChannel: '102 112 133',
|
||||
disabled: 'var(--mui-palette-neutral-400)',
|
||||
},
|
||||
warning: {
|
||||
...california,
|
||||
light: california[400],
|
||||
main: california[500],
|
||||
dark: california[600],
|
||||
contrastText: 'var(--mui-palette-common-white)',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
97
front/src/styles/theme/colors.js
Normal file
97
front/src/styles/theme/colors.js
Normal file
@@ -0,0 +1,97 @@
|
||||
export const california = {
|
||||
50: '#fffaea',
|
||||
100: '#fff3c6',
|
||||
200: '#ffe587',
|
||||
300: '#ffd049',
|
||||
400: '#ffbb1f',
|
||||
500: '#fb9c0c',
|
||||
600: '#de7101',
|
||||
700: '#b84d05',
|
||||
800: '#953b0b',
|
||||
900: '#7b310c',
|
||||
950: '#471701',
|
||||
};
|
||||
|
||||
export const kepple = {
|
||||
50: '#f0fdfa',
|
||||
100: '#ccfbef',
|
||||
200: '#9af5e1',
|
||||
300: '#5fe9ce',
|
||||
400: '#2ed3b8',
|
||||
500: '#15b79f',
|
||||
600: '#0e9382',
|
||||
700: '#107569',
|
||||
800: '#115e56',
|
||||
900: '#134e48',
|
||||
950: '#042f2c',
|
||||
};
|
||||
|
||||
export const neonBlue = {
|
||||
50: '#ecf0ff',
|
||||
100: '#dde3ff',
|
||||
200: '#c2cbff',
|
||||
300: '#9ca7ff',
|
||||
400: '#7578ff',
|
||||
500: '#635bff',
|
||||
600: '#4e36f5',
|
||||
700: '#432ad8',
|
||||
800: '#3725ae',
|
||||
900: '#302689',
|
||||
950: '#1e1650',
|
||||
};
|
||||
|
||||
export const nevada = {
|
||||
50: '#fbfcfe',
|
||||
100: '#f0f4f8',
|
||||
200: '#dde7ee',
|
||||
300: '#cdd7e1',
|
||||
400: '#9fa6ad',
|
||||
500: '#636b74',
|
||||
600: '#555e68',
|
||||
700: '#32383e',
|
||||
800: '#202427',
|
||||
900: '#121517',
|
||||
950: '#090a0b',
|
||||
};
|
||||
|
||||
export const redOrange = {
|
||||
50: '#fef3f2',
|
||||
100: '#fee4e2',
|
||||
200: '#ffcdc9',
|
||||
300: '#fdaaa4',
|
||||
400: '#f97970',
|
||||
500: '#f04438',
|
||||
600: '#de3024',
|
||||
700: '#bb241a',
|
||||
800: '#9a221a',
|
||||
900: '#80231c',
|
||||
950: '#460d09',
|
||||
};
|
||||
|
||||
export const shakespeare = {
|
||||
50: '#ecfdff',
|
||||
100: '#cff7fe',
|
||||
200: '#a4eefd',
|
||||
300: '#66e0fa',
|
||||
400: '#10bee8',
|
||||
500: '#04aad6',
|
||||
600: '#0787b3',
|
||||
700: '#0d6d91',
|
||||
800: '#145876',
|
||||
900: '#154964',
|
||||
950: '#082f44',
|
||||
};
|
||||
|
||||
export const stormGrey = {
|
||||
50: '#f9fafb',
|
||||
100: '#f1f1f4',
|
||||
200: '#dcdfe4',
|
||||
300: '#b3b9c6',
|
||||
400: '#8a94a6',
|
||||
500: '#667085',
|
||||
600: '#565e73',
|
||||
700: '#434a60',
|
||||
800: '#313749',
|
||||
900: '#212636',
|
||||
950: '#121621',
|
||||
};
|
||||
3
front/src/styles/theme/components/avatar.js
Normal file
3
front/src/styles/theme/components/avatar.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export const MuiAvatar = {
|
||||
styleOverrides: { root: { fontSize: '14px', fontWeight: 600, letterSpacing: 0 } },
|
||||
};
|
||||
11
front/src/styles/theme/components/button.js
Normal file
11
front/src/styles/theme/components/button.js
Normal file
@@ -0,0 +1,11 @@
|
||||
export const MuiButton = {
|
||||
styleOverrides: {
|
||||
root: {borderRadius: '12px', textTransform: 'none'},
|
||||
sizeSmall: {padding: '6px 16px'},
|
||||
sizeMedium: {padding: '8px 20px'},
|
||||
sizeLarge: {padding: '11px 24px'},
|
||||
textSizeSmall: {padding: '7px 12px'},
|
||||
textSizeMedium: {padding: '9px 16px'},
|
||||
textSizeLarge: {padding: '12px 16px'},
|
||||
},
|
||||
};
|
||||
3
front/src/styles/theme/components/card-content.js
Normal file
3
front/src/styles/theme/components/card-content.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export const MuiCardContent = {
|
||||
styleOverrides: {root: {padding: '32px 24px', '&:last-child': {paddingBottom: '32px'}}},
|
||||
};
|
||||
4
front/src/styles/theme/components/card-header.js
Normal file
4
front/src/styles/theme/components/card-header.js
Normal file
@@ -0,0 +1,4 @@
|
||||
export const MuiCardHeader = {
|
||||
defaultProps: {titleTypographyProps: {variant: 'h6'}, subheaderTypographyProps: {variant: 'body2'}},
|
||||
styleOverrides: {root: {padding: '32px 24px 16px'}},
|
||||
};
|
||||
17
front/src/styles/theme/components/card.js
Normal file
17
front/src/styles/theme/components/card.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import {paperClasses} from '@mui/material/Paper';
|
||||
|
||||
export const MuiCard = {
|
||||
styleOverrides: {
|
||||
root: ({theme}) => {
|
||||
return {
|
||||
borderRadius: '20px',
|
||||
[`&.${paperClasses.elevation1}`]: {
|
||||
boxShadow:
|
||||
theme.palette.mode === 'dark'
|
||||
? '0 5px 22px 0 rgba(0, 0, 0, 0.24), 0 0 0 1px rgba(255, 255, 255, 0.12)'
|
||||
: '0 5px 22px 0 rgba(0, 0, 0, 0.04), 0 0 0 1px rgba(0, 0, 0, 0.06)',
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
25
front/src/styles/theme/components/components.js
Normal file
25
front/src/styles/theme/components/components.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import {MuiAvatar} from './avatar';
|
||||
import {MuiButton} from "./button";
|
||||
import {MuiCard} from "./card";
|
||||
import {MuiCardContent} from "./card-content";
|
||||
import {MuiCardHeader} from "./card-header";
|
||||
import {MuiLink} from "./link";
|
||||
import {MuiStack} from "./stack";
|
||||
import {MuiTab} from "./tab";
|
||||
import {MuiTableBody} from "./table-body";
|
||||
import {MuiTableCell} from "./table-cell";
|
||||
import {MuiTableHead} from "./table-head";
|
||||
|
||||
export const components = {
|
||||
MuiAvatar,
|
||||
MuiButton,
|
||||
MuiCard,
|
||||
MuiCardContent,
|
||||
MuiCardHeader,
|
||||
MuiLink,
|
||||
MuiStack,
|
||||
MuiTab,
|
||||
MuiTableBody,
|
||||
MuiTableCell,
|
||||
MuiTableHead,
|
||||
};
|
||||
8
front/src/styles/theme/components/link.js
Normal file
8
front/src/styles/theme/components/link.js
Normal file
@@ -0,0 +1,8 @@
|
||||
export const MuiLink = {
|
||||
defaultProps: {underline: 'hover'},
|
||||
styleOverrides: {
|
||||
root: {
|
||||
color: 'var(--mui-palette-text-primary)'
|
||||
}
|
||||
}
|
||||
};
|
||||
1
front/src/styles/theme/components/stack.js
Normal file
1
front/src/styles/theme/components/stack.js
Normal file
@@ -0,0 +1 @@
|
||||
export const MuiStack = {defaultProps: {useFlexGap: true}};
|
||||
14
front/src/styles/theme/components/tab.js
Normal file
14
front/src/styles/theme/components/tab.js
Normal file
@@ -0,0 +1,14 @@
|
||||
export const MuiTab = {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
fontSize: '14px',
|
||||
fontWeight: 500,
|
||||
lineHeight: 1.71,
|
||||
minWidth: 'auto',
|
||||
paddingLeft: 0,
|
||||
paddingRight: 0,
|
||||
textTransform: 'none',
|
||||
'& + &': {marginLeft: '24px'},
|
||||
},
|
||||
},
|
||||
};
|
||||
10
front/src/styles/theme/components/table-body.js
Normal file
10
front/src/styles/theme/components/table-body.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import {tableCellClasses} from '@mui/material/TableCell';
|
||||
import {tableRowClasses} from '@mui/material/TableRow';
|
||||
|
||||
export const MuiTableBody = {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
[`& .${tableRowClasses.root}:last-child`]: {[`& .${tableCellClasses.root}`]: {'--TableCell-borderWidth': 0}},
|
||||
},
|
||||
},
|
||||
};
|
||||
6
front/src/styles/theme/components/table-cell.js
Normal file
6
front/src/styles/theme/components/table-cell.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export const MuiTableCell = {
|
||||
styleOverrides: {
|
||||
root: {borderBottom: 'var(--TableCell-borderWidth, 1px) solid var(--mui-palette-TableCell-border)'},
|
||||
paddingCheckbox: {padding: '0 0 0 24px'},
|
||||
},
|
||||
};
|
||||
13
front/src/styles/theme/components/table-head.js
Normal file
13
front/src/styles/theme/components/table-head.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import {tableCellClasses} from '@mui/material/TableCell';
|
||||
|
||||
export const MuiTableHead = {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
[`& .${tableCellClasses.root}`]: {
|
||||
backgroundColor: 'var(--mui-palette-background-level1)',
|
||||
color: 'var(--mui-palette-text-secondary)',
|
||||
lineHeight: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
16
front/src/styles/theme/create-theme.js
Normal file
16
front/src/styles/theme/create-theme.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import {experimental_extendTheme as extendTheme} from '@mui/material/styles';
|
||||
import {components} from "./components/components";
|
||||
import shadows from "@mui/material/styles/shadows";
|
||||
import {typography} from "./typography";
|
||||
import {colorSchemes} from "./color-schemes";
|
||||
|
||||
export function createTTheme() {
|
||||
return extendTheme({
|
||||
breakpoints: {values: {xs: 0, sm: 450, md: 600, lg: 900, xl: 1440}},
|
||||
colorSchemes: colorSchemes,
|
||||
components: components,
|
||||
shadows: shadows,
|
||||
shape: {borderRadius: 8},
|
||||
typography: typography,
|
||||
});
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user