Initial commit

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

7
front/src/Config.js Normal file
View File

@@ -0,0 +1,7 @@
import {getSiteURL} from "./lib/getSiteUrl";
import {LogLevel} from "./lib/Logger";
export const config = {
site: {name: 'Bar', description: '', themeColor: '#090a0b', url: getSiteURL()},
logLevel: (process.env.NEXT_PUBLIC_LOG_LEVEL) ?? LogLevel.ALL,
};

5
front/src/Dockerfile Normal file
View File

@@ -0,0 +1,5 @@
FROM nginx:alpine as nginx
WORKDIR /app
COPY ../nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
CMD "nginx" "-g" "daemon off;"

51
front/src/app/App.js Normal file
View File

@@ -0,0 +1,51 @@
import {CssBaseline, GlobalStyles} from "@mui/material";
import {LocalizationProvider} from "../components/core/LocalizationProvider";
import {AuthProvider} from "../context/AuthContext";
import {createTTheme} from "../styles/theme/create-theme";
import {Experimental_CssVarsProvider as CssVarsProvider} from '@mui/material/styles';
import {BrowserRouter as Router} from "react-router-dom";
import {NavigationRoutes} from "./NavigationRoutes";
import {SnackbarProvider} from 'notistack';
import {UserProvider} from "../context/UserContext";
function App() {
const theme = createTTheme();
return (
// Провайдер времени
<LocalizationProvider>
{/*Провайдер уведомлений*/}
<SnackbarProvider maxSnack={6} anchorOrigin={{vertical: 'bottom', horizontal: 'right'}}
style={{borderRadius: '10px'}}>
{/*Провайдер авторизации*/}
<AuthProvider>
{/*Провайдер пользователя*/}
<UserProvider>
{/*Провайдер темы*/}
<CssVarsProvider theme={theme}>
<CssBaseline/>
<GlobalStyles
styles={{
body: {
'--MainNav-height': '56px',
'--MainNav-zIndex': 1000,
'--SideNav-width': '280px',
'--SideNav-zIndex': 1200,
'--MobileNav-width': '320px',
'--MobileNav-zIndex': 1200,
},
}}
/>
{/*Маршрутизация*/}
<Router>
<NavigationRoutes/>
</Router>
</CssVarsProvider>
</UserProvider>
</AuthProvider>
</SnackbarProvider>
</LocalizationProvider>
);
}
export default App;

View File

@@ -0,0 +1,10 @@
import {paths} from "../path";
import {Loading} from "../components/core/Loading";
export function HomeRedirect({auth}) {
const redirectPath = auth ? paths.dashboard.overview : paths.auth.signIn;
window.location.replace(redirectPath);
return (
<Loading loading={true}/>
)
}

View File

@@ -0,0 +1,151 @@
import {Route, Routes} from "react-router-dom";
import {paths} from "../path";
import {useAuth} from "../hooks/useAuth";
import NotFoundPage from "./pages/notFound/NotFoundPage";
import {UserLayout} from "./layout/UserLayout";
import {HomeRedirect} from "./HomeRedirect";
import {PublicLayout} from "./layout/PublicLayout";
import QueuePage from "./pages/queue/QueuePage";
import LoginPage from "./pages/auth/sign-in/loginPage";
import {TelegramCode} from "./pages/auth/sign-in/telegram-code";
import {IngredientsPage} from "./pages/ingredients/IngredientsPage";
import {MenuPage} from "./pages/cocktails/MenuPage";
import {AllCocktailsPage} from "./pages/cocktails/AllCocktailsPage";
import {EditIngredientPage} from "./pages/ingredients/EditIngredientPage";
import {EditCocktailPage} from "./pages/cocktails/EditCocktailPage";
import {MyQueuePage} from "./pages/queue/MyQueuePage";
import {VisitorPage} from "./pages/VisitorPage";
import {CocktailMenuBarPage} from "./pages/cocktails/CocktailMenuBarPage";
import {MyBarPage} from "./pages/MyBarPage";
import {useEffect, useState} from "react";
export function NavigationRoutes() {
const {auth} = useAuth();
const [loadedRoutes, setLoadedRoutes] = useState(undefined);
useEffect(() => {
setLoadedRoutes(auth ? authPages : guestPages)
}, [auth]);
if (!loadedRoutes) {
return null
}
return (
<Routes>
{loadedRoutes.map((page) => {
return (
<Route
key={page.path + page.isPrivate + page.exact}
path={page.path}
exact={page.exact}
element={<ElementProvider isPrivate={page.isPrivate}>
{page.children}
</ElementProvider>}/>
)
})}
</Routes>
)
}
function ElementProvider({isPrivate, children}) {
if (isPrivate) {
return (<UserLayout>{children}</UserLayout>);
} else {
return (<PublicLayout>{children}</PublicLayout>);
}
}
const authPages = [
{
children: (<HomeRedirect auth={true}/>),
isPrivate: false,
path: paths.home,
},
{
path: paths.auth.signIn,
children: (<LoginPage/>),
isPrivate: false,
},
{
path: paths.dashboard.overview,
isPrivate: true,
children: (<MenuPage/>),
exact: true,
},
{
path: paths.bar.cocktails,
isPrivate: true,
children: (<AllCocktailsPage/>)
},
{
path: paths.bar.list,
isPrivate: true,
children: (<MyBarPage/>)
},
{
path: paths.orders.my,
isPrivate: true,
children: (<MyQueuePage/>)
},
{
path: paths.bar.ingredients,
isPrivate: true,
children: (<IngredientsPage/>)
},
{
path: paths.bar.ordersQueue,
isPrivate: true,
children: (<QueuePage/>),
},
{
path: paths.visitor.inBar,
isPrivate: true,
children: (<VisitorPage/>)
},
{
path: paths.bar.ingredientEdit,
isPrivate: true,
forAdmin: true,
children: (<EditIngredientPage/>)
},
{
path: paths.bar.menu,
isPrivate: true,
children: (<CocktailMenuBarPage/>)
},
{
path: paths.bar.cocktailEdit,
isPrivate: true,
forAdmin: true,
children: (<EditCocktailPage/>)
},
{
path: paths.notFound,
isPrivate: false,
children: (<NotFoundPage/>)
},
]
const guestPages = [
{
path: paths.home,
isPrivate: false,
children: (<HomeRedirect auth={false}/>),
exact: true,
},
{
path: paths.auth.tg,
isPrivate: false,
children: (<TelegramCode/>),
exact: false
},
{
path: paths.auth.signIn,
isPrivate:
false,
children: (<LoginPage/>),
},
{
path: paths.notFound,
isPrivate: false,
children: (<NotFoundPage/>),
},
]

View File

@@ -0,0 +1,58 @@
import * as React from 'react';
import Box from '@mui/material/Box';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import {DynamicLogo} from "../../components/core/Logo";
import {paths} from "../../path";
export function PublicLayout({ children }) {
return (
<Box
sx={{
display: { xs: 'flex', lg: 'grid' },
flexDirection: 'column',
gridTemplateColumns: '1fr 1fr',
}}
>
<Box sx={{ display: 'flex', flex: '1 1 auto', flexDirection: 'column' }}>
<Box sx={{ p: 3 }}>
<Box component={'a'} href={paths.home} sx={{ display: 'inline-block', fontSize: 0 }}>
<DynamicLogo colorDark="light" colorLight="dark" height={32} width={122} />
</Box>
</Box>
<Box sx={{ alignItems: 'center', display: 'flex', flex: '1 1 auto', justifyContent: 'center', p: 3 }}>
<Box sx={{ maxWidth: '450px', width: '100%' }}>{children}</Box>
</Box>
</Box>
<Box
sx={{
alignItems: 'center',
background: 'radial-gradient(50% 50% at 50% 50%, #122647 0%, #090E23 100%)',
color: 'var(--mui-palette-common-white)',
display: { xs: 'none', lg: 'flex' },
justifyContent: 'center',
p: 3,
}}
>
<Stack spacing={3}>
<Stack spacing={1}>
<Typography color="inherit" sx={{ fontSize: '24px', lineHeight: '32px', textAlign: 'center' }} variant="h1">
<Box component="span" sx={{ color: '#15b79e' }}>
Добро пожаловать в бар
</Box>
</Typography>
<Typography align="center" variant="subtitle1">
Самый большой выбор честно спизженных коктейлей
</Typography>
<Box
component="img"
alt="Under development"
src="/assets/qr.png"
sx={{ display: 'inline-block', height: 'auto', maxWidth: '100%', width: '400px' }}
/>
</Stack>
</Stack>
</Box>
</Box>
);
}

View File

@@ -0,0 +1,31 @@
import {SideNav} from "../../components/navigation/SideNav";
import Box from "@mui/material/Box";
import {MainNav} from "../../components/navigation/MainNav";
import Container from "@mui/material/Container";
export function UserLayout({children}) {
return (
<Box
sx={{
bgcolor: 'var(--mui-palette-background-default)',
display: 'flex',
flexDirection: 'column',
position: 'relative',
minHeight: '100%',
}}
>
<SideNav/>
<Box sx={{
display: 'flex',
flex: '1 1 auto',
flexDirection: 'column',
pl: {xl: 'var(--SideNav-width)'}
}}>
<MainNav/>
<Container maxWidth="xl" sx={{py: '16px'}}>
{children}
</Container>
</Box>
</Box>
)
}

View File

@@ -0,0 +1,62 @@
import Box from "@mui/material/Box";
import Toolbar from "@mui/material/Toolbar";
import Typography from "@mui/material/Typography";
import Paper from "@mui/material/Paper";
import {FormControl, InputAdornment, InputLabel, OutlinedInput, Tabs} from "@mui/material";
import IconButton from "@mui/material/IconButton";
import SearchIcon from "@mui/icons-material/Search";
import * as React from "react";
import {useState} from "react";
import Tab from "@mui/material/Tab";
import {a11yProps} from "../../components/core/tabProps";
import {CustomTabPanel} from "../../components/core/TabPanel";
import {BarList} from "../../components/bar/BarList";
export function MyBarPage() {
const [value, setValue] = React.useState(0);
const handleChange = (event, newValue) => setValue(newValue);
const [findString, setFindString] = useState("");
return (
<Box>
{/*Заголовок*/}
<Toolbar>
<Typography variant="h6" component="div" sx={{flexGrow: 1}}>Мои бары</Typography>
</Toolbar>
{/*Поиск*/}
<Paper elevation={6} sx={{my: 2}}>
<FormControl sx={{m: 1, width: 'calc(100% - 20px'}}>
<InputLabel htmlFor="outlined-adornment-amount">Поиск</InputLabel>
<OutlinedInput
onChange={(e) => setFindString(e.target.value)}
label="With normal TextField"
startAdornment={
<InputAdornment position="start">
<IconButton edge="end">
<SearchIcon/>
</IconButton>
</InputAdornment>
}
/>
</FormControl>
</Paper>
{/*Рабочее поле ингредиентов*/}
<Box>
<Tabs value={value} onChange={handleChange} aria-label="basic tabs example">
<Tab label="Мои бары" {...a11yProps(0)} />
<Tab label="Список" {...a11yProps(1)} />
</Tabs>
</Box>
<Box>
<CustomTabPanel value={value} index={0}>
<BarList all={false} find={findString}/>
</CustomTabPanel>
<CustomTabPanel value={value} index={1}>
<BarList all={true} find={findString}/>
</CustomTabPanel>
</Box>
{/*Модальное окно информации об ингредиенте*/}
{/*<IngredientInfoModal ingredient={selectedInfo} open={openModal} closeHandler={handleCloseModal}/>*/}
</Box>
)
}

View File

@@ -0,0 +1,77 @@
import Box from "@mui/material/Box";
import {useEffect, useState} from "react";
import {api} from "../../lib/clients/api";
import {requests} from "../../requests";
import {useAlert} from "../../hooks/useAlert";
import Typography from "@mui/material/Typography";
import {VisitorItem} from "../../components/visitor/VisitorItem";
import Toolbar from "@mui/material/Toolbar";
import * as React from "react";
import Button from "@mui/material/Button";
import {useUser} from "../../hooks/useUser";
export function VisitorPage() {
const {session, checkSession} = useUser();
const [visitors, setVisitors] = useState([])
const [open, setOpen] = useState(false);
const {createError} = useAlert();
useEffect(() => {
api().get(requests.visitors.all)
.then((r) => {
setVisitors(r.data)
})
.catch(() => createError("Ошибка получения данных"))
// eslint-disable-next-line
}, []);
useEffect(() => {
setOpen(session.isActive);
}, [session, checkSession])
const changeHandler = (visitor) => {
const arr = visitors.map((v) => {
if(v.id === visitor.id) {
return {
...visitor,
invited: !visitor.invited
}
}
return v;
})
api().post(`${requests.visitors.invite}id=${visitor.id}&value=${!visitor.invited}`)
.then(() => setVisitors(arr))
.catch(() => createError("Ошибка запроса"))
}
const changeShift = () => {
api().post(`${requests.bar.session.change}?value=${!open}`)
.then(() => {
checkSession?.();
setOpen(!open)
})
.catch(() => createError("Ошибка закрытия сессии"))
}
return (
<Box>
{/*Заголовок*/}
<Toolbar>
<Typography variant="h6" component="div" sx={{flexGrow: 1}}>Посетители</Typography>
</Toolbar>
<Box ml={0} mb={2}>
{visitors.map((v) => {
return (
<VisitorItem key={v.id} visitor={v} changeHandler={changeHandler} open={open}/>
)
})}
</Box>
<Button
variant='contained'
color={open ? 'error' : 'success'}
onClick={() => changeShift()}
>{`${open ? "Закрыть " : "Открыть "}смену`}</Button>
</Box>
)
}

View File

@@ -0,0 +1,11 @@
import * as React from 'react';
import {GuestGuard} from "../../../../components/auth/guest-guard";
import {SignInForm} from "../../../../components/auth/sign-in-form";
export default function LoginPage() {
return (
<GuestGuard>
<SignInForm/>
</GuestGuard>
);
}

View File

@@ -0,0 +1,30 @@
import * as React from "react";
import {useSearchParams} from "react-router-dom";
import {Loading} from "../../../../components/core/Loading";
import {api} from "../../../../lib/clients/api";
import {requests} from "../../../../requests";
import {useAuth} from "../../../../hooks/useAuth";
export function TelegramCode() {
const [searchParams] = useSearchParams();
const {checkSession} = useAuth();
let code = searchParams.get("code");
const request = {
byLogin: false,
code: code
}
api().post(requests.auth.login, request)
.then(async (response) => {
if (response.data.error) {
return;
}
localStorage.setItem("token", response.data.token);
await checkSession?.();
window.location.reload();
})
return (
<Loading loading={true}/>
)
}

View File

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

View File

@@ -0,0 +1,127 @@
import Box from "@mui/material/Box";
import Toolbar from "@mui/material/Toolbar";
import Typography from "@mui/material/Typography";
import Paper from "@mui/material/Paper";
import {Fab, FormControl, FormControlLabel, InputAdornment, InputLabel, OutlinedInput} from "@mui/material";
import IconButton from "@mui/material/IconButton";
import SearchIcon from "@mui/icons-material/Search";
import Switch from "@mui/material/Switch";
import {blue} from "@mui/material/colors";
import UpIcon from "@mui/icons-material/KeyboardArrowUp";
import {Loading} from "../../../components/core/Loading";
import * as React from "react";
import {useEffect, useMemo, useState} from "react";
import {CocktailsList} from "../../../components/cocktails/CocktailsList";
import {requests} from "../../../requests";
import {api} from "../../../lib/clients/api";
import {useAlert} from "../../../hooks/useAlert";
import {CocktailInfoModal} from "../../../components/cocktails/CocktailInfoModal";
export function CocktailMenuBarPage() {
const {createError} = useAlert();
const [grouping, setGrouping] = useState(true);
const [findString, setFindString] = useState("");
const [loading, setLoading] = useState(true);
const [cocktails, setCocktails] = useState([]);
const [openModal, setOpenModal] = useState(false);
const [selected, setSelected] = useState(null);
useEffect(() => {
api().get(`${requests.cocktails.menu}?all=true`)
.then((r) => {
setCocktails(r.data);
setLoading(false);
})
.catch(() => createError("Ошибка получения данных"))
// eslint-disable-next-line
}, []);
const handleOpenModal = (row) => {
setSelected(row)
setOpenModal(true);
}
const changeHandler = (row, value) => {
const newState = cocktails.map((r) => {
if(r.id !== row.id) {
return r;
}
return {
...r,
inMenu: value
}
});
api().post(`${requests.cocktails.menu}?id=${row.id}&value=${value}`)
.then(() => {
setCocktails(newState);
}).catch(() => createError("Ошибка сохранения данных"))
}
const visibleRows = useMemo(() => {
if (findString === "") {
return cocktails;
}
let regExp = new RegExp("(.*?)" + findString + "(.*?)", "i");
return cocktails
.filter((row) => row.name.split(" ").map((n) => n.match(regExp) !== null).includes(true))
// eslint-disable-next-line
}, [cocktails, findString])
return (
<Box>
{/*Заголовок*/}
<Toolbar>
<Typography variant="h6" component="div" sx={{flexGrow: 1}}>Меню бара</Typography>
</Toolbar>
{/*Поиск*/}
<Paper elevation={6} sx={{my: 2}}>
<FormControl sx={{m: 1, width: 'calc(100% - 20px'}}>
<InputLabel htmlFor="outlined-adornment-amount">Поиск</InputLabel>
<OutlinedInput
onChange={(e) => setFindString(e.target.value)}
label="With normal TextField"
startAdornment={
<InputAdornment position="start">
<IconButton edge="end">
<SearchIcon/>
</IconButton>
</InputAdornment>
}
/>
</FormControl>
<FormControlLabel sx={{ml: '2px'}}
control={<Switch defaultChecked/>}
onClick={() => setGrouping(!grouping)}
label="Группировать"
labelPlacement="end"/>
</Paper>
{/*Рабочее поле коктейлей*/}
<CocktailsList rows={visibleRows} changeHandler={changeHandler}
infoHandler={handleOpenModal} grouping={grouping}/>
{/*Иконка возврата наверх*/}
<Fab sx={{
alpha: '30%',
position: 'sticky',
bottom: '16px',
color: 'common.white',
bgcolor: blue[600],
'&:hover': {
bgcolor: blue[600],
},
}}
onClick={() => window.window.scrollTo(0, 0)}
aria-label='Expand'
color='inherit'>
<UpIcon/>
</Fab>
{/*Загрузчик*/}
<Loading loading={loading}/>
{/*Модальное окно информации об ингредиенте*/}
<CocktailInfoModal open={openModal} row={selected}
closeHandler={() => {
setSelected(null);
setOpenModal(false);
}}/>
</Box>
)
}

View File

@@ -0,0 +1,333 @@
import Grid from "@mui/material/Grid";
import {useAlert} from "../../../hooks/useAlert";
import * as React from "react";
import {useCallback, useEffect, useState} from "react";
import {Cocktail} from "../../../components/cocktails/Cocktail";
import {Fab, Skeleton} from "@mui/material";
import Box from "@mui/material/Box";
import {requests} from "../../../requests";
import {NoResult} from "../../../components/cocktails/NoResult";
import {FilterBlock} from "../../../components/cocktails/FilterBlock";
import {api} from "../../../lib/clients/api";
import {CocktailInfoModal} from "../../../components/cocktails/CocktailInfoModal";
import {useUser} from "../../../hooks/useUser";
import {blue} from "@mui/material/colors";
import UpIcon from "@mui/icons-material/KeyboardArrowUp";
import {sortList} from "../../../components/cocktails/sortingList";
import {getComparator} from "../../../components/core/getComparator";
import Button from "@mui/material/Button";
import Paper from "@mui/material/Paper";
import CheckMarks from "../../../components/cocktails/CheckMarks";
const filterList = (rows, filter, allowIngredients) => {
let regExp = new RegExp("(.*?)" + filter.search + "(.*?)", "i");
const sortingObj = sortList.find((s) => s.name === filter.sorting);
const sortingValues = sortingObj.id.split("|");
return rows
.filter((row) => {
const nameReg = row.name.split(" ").map((n) => n.match(regExp) !== null).includes(true);
const ingredientReg = row.components
.split(", ")
.map((r) => r.match(regExp) !== null)
.includes(true);
return nameReg || ingredientReg;
})
.filter((row) => filter.onlyFavourite ? row.rating.favourite : true)
.filter((row) => filter.glass.length === 0 || filter.glass.includes(row.glass))
.filter((row) => filter.category.length === 0 || filter.category.includes(row.category))
.filter((row) => filter.alcohol.length === 0 || filter.alcohol.includes(row.alcoholic))
.filter((row) => {
if (filter.tags.length === 0) {
return true;
}
if (row.tags.length === 0) {
return false;
}
return row.tags.split(",").find((tag) => filter.tags.includes(tag))
})
.filter((row) => {
if (filter.iCount.length === 0) {
return true;
}
const arr = row.components.split(", ");
const count = arr.filter((n) => !allowIngredients.includes(n)).length;
const filt = filter.ingredient.length === 0 || arr.filter((n) => filter.ingredient.includes(n)).length > 0;
return filter.iCount === count && filt;
})
.filter((row) => {
if (filter.inMenu === "") {
return row;
}
const filterValue = filter.inMenu === "Есть в меню";
return filterValue === row.inMenu;
})
.sort(getComparator(sortingValues[1], sortingValues[0], "name"))
}
const emptyFilter = {
search: "",
hidden: true,
onlyFavourite: false,
glass: [],
category: [],
alcohol: [],
tags: [],
iCount: [],
ingredient: [],
inMenu: "",
sorting: "Название по возрастанию"
}
const CocktailsPageContent = ({all}) => {
const {user} = useUser();
const {createError, createSuccess} = useAlert();
const [allowIngredients, setAllowIngredients] = useState([])
const [rows, setRows] = useState([]);
const [filter, setFilter] = useState(emptyFilter)
const [open, setOpen] = useState(false);
const [selectedCocktail, setSelectedCocktail] = useState(null)
const [chips, setChips] = useState([])
const [page, setPage] = useState(-1);
const [load, setLoad] = useState(false);
const [isEnd, setIsEnd] = useState(false);
const [isNew, setIsNew] = useState(true);
const loading = useCallback(() => {
const size = Math.floor((window.innerWidth) / 350) * 5;
if (load || (!isNew && isEnd)) {
return false;
}
setLoad(true);
const request = {
...filter,
all: all,
sort: sortList.find((s) => s.name === filter.sorting).id,
page: page + 1,
size: size,
iCount: Array.isArray(filter.iCount) ? null : filter.iCount
}
api().post(requests.cocktails.menu, request)
.then((r) => {
if (r.data.length === 0) {
if(isNew) {
setRows([]);
}
setIsEnd(true);
setLoad(false);
return;
}
const cocktails = isNew ? r.data : rows.concat(r.data);
setRows(cocktails);
setIsNew(false);
setPage(page + 1);
setLoad(false);
})
.catch((r) => {
setLoad(false);
createError("Ошибка загрузки данных от сервера Status:" + r.status)
})
// eslint-disable-next-line
}, [load, isEnd, page]);
useEffect(() => {
const handleScroll = () => {
const {scrollTop, scrollHeight, clientHeight} = document.documentElement;
if (scrollTop + clientHeight >= scrollHeight - 100) {
loading();
}
}
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, [loading]);
useEffect(() => {
api().get(requests.bar.ingredientSimple)
.then((r) => {
const arr = r.data.filter((i) => i.isHave)
.map((i) => i.name)
setAllowIngredients(arr)
})
.catch(() => createError("Ошибка получения ингредиентов"))
// eslint-disable-next-line
}, [])
useEffect(() => {
loading();
}, [filter])
useEffect(() => {
if (!all) {
return;
}
const ingredients = new Set();
rows.map((c) => c.components)
.map((c) => c.split(", "))
.map((c) => c.filter((i) => !allowIngredients.includes(i)))
.filter((nhc) => nhc.length === 1)
.map((fc) => fc[0])
.forEach((i) => ingredients.add(i))
setChips(Array.from(ingredients).sort(getComparator()));
}, [rows, allowIngredients])
const renderSkeleton = () => {
return Array.from({length: 3}, () => null)
.map((v, index) => <Skeleton sx={{m: 2}}
key={index}
variant="rounded"
width={350}
height={690}/>);
}
const handleChangeRating = (row, value) => {
const newState = rows.map((r) => {
if (row.id === r.id) {
let newRating = r.rating;
newRating.rating = value
return {
...r,
rating: newRating
}
}
return r;
})
api().post(`${requests.cocktails.rating}${row.id}&rating=${value}`)
.then(() => {
setRows(newState);
createSuccess("Спасибо за оценку!")
}).catch(() => createError("Ошибка сохранения"))
}
const handleFilterChange = (filterName, value) => {
const newState = {
...filter,
[filterName]: value
}
setFilter(newState)
setIsNew(true);
setIsEnd(false);
setPage(-1);
}
const handleFavourite = (row) => {
const value = !row.rating.favourite;
const newState = rows.map((r) => {
if (r.id === row.id) {
let newRating = r.rating;
newRating.favourite = value;
return {
...r,
rating: newRating
}
}
return r;
});
let url = `${requests.cocktails.favourite}${row.id}`;
let request = value ? api().put(url) : api().delete(url);
request
.then(() => {
setRows(newState);
createSuccess("Спасибо за оценку!")
}).catch(() => createError("Ошибка сохранения"))
}
const handleFilterClear = () => {
setFilter(emptyFilter);
}
const handleSelectCocktail = (row) => {
setSelectedCocktail(row.id)
setOpen(true)
}
const handleCloseCocktailModal = () => {
setOpen(false);
setSelectedCocktail(null);
}
const handleEditMenu = (row, value) => {
const newState = rows.map((r) => {
if (r.id !== row.id) {
return r;
}
if (all) {
return {
...r,
inMenu: value
}
}
return null
}).filter((r) => r !== null);
api().post(`${requests.cocktails.menu}?id=${row.id}&value=${value}`)
.then(() => setRows(newState))
.catch(() => createError("Ошибка сохранения данных"))
}
const editMenuBlock = (row) => {
if (user.role === "USER" || user.role === "ADMIN_NOT_BARMEN") {
return null;
}
return (
<Button color={row.inMenu ? 'error' : 'success'} variant='contained'
onClick={() => handleEditMenu(row, !row.inMenu)}>
{(row.inMenu ? 'Удалить из' : 'Добавить в') + ' меню'}
</Button>
)
}
return (
<Box>
{/*<Loading loading={load}/>*/}
{/*Модальное окно информации о коктейле*/}
<CocktailInfoModal row={selectedCocktail} open={open}
closeHandler={handleCloseCocktailModal}/>
{/*Блок фильтров*/}
<FilterBlock
filter={filter}
handleFilterChange={handleFilterChange}
handleClearFilter={handleFilterClear}
barmen={user.role !== 'USER'}
all={all}
/>
{/*todo: доделать фильтр по количеству недостающих ингредиентов*/}
{/*{*/}
{/* (all && filter.iCount === 1) && (*/}
{/* <Paper sx={{mt: 1}}>*/}
{/* <CheckMarks rows={chips} name={"Выбор ингредиента"} filterName={"ingredient"}*/}
{/* filterValue={filter.ingredient}*/}
{/* handleChange={handleFilterChange}*/}
{/* identity*/}
{/* />*/}
{/* </Paper>*/}
{/* )*/}
{/*}*/}
<Box>
{/*Основное содержимое*/}
<Grid container rowSpacing={2} columnSpacing={{xs: 1, sm: 1, md: 2}} sx={{m: 1}}>
{rows.length > 0 && rows.map((row) => {
return (
<Cocktail key={row.id} row={row} handleFavourite={handleFavourite}
handleChangeRating={handleChangeRating}
handleSelect={handleSelectCocktail}
editMenuBlock={editMenuBlock}
/>
)
})}
{load && renderSkeleton()}
{rows.length === 0 && (<NoResult/>)}
</Grid>
</Box>
<Fab sx={{
alpha: '30%',
position: 'sticky',
left: 'calc(100% - 16px)',
bottom: '16px',
color: 'common.white',
bgcolor: blue[600],
'&:hover': {
bgcolor: blue[600],
},
}}
onClick={() => window.window.scrollTo(0, 0)}
aria-label='Expand'
color='inherit'>
<UpIcon/>
</Fab>
</Box>
);
}
export default CocktailsPageContent;

View File

@@ -0,0 +1,258 @@
import Box from "@mui/material/Box";
import Toolbar from "@mui/material/Toolbar";
import Typography from "@mui/material/Typography";
import * as React from "react";
import {useEffect, useState} from "react";
import Paper from "@mui/material/Paper";
import {Autocomplete} from "@mui/material";
import TextField from "@mui/material/TextField";
import {api} from "../../../lib/clients/api";
import {requests} from "../../../requests";
import {useAlert} from "../../../hooks/useAlert";
import Stack from "@mui/material/Stack";
import Button from "@mui/material/Button";
import CheckMarks from "../../../components/cocktails/CheckMarks";
import {EditCocktailReceipt} from "../../../components/cocktails/EditCocktailReceipt";
import {SelectEdit} from "../../../components/cocktails/SelectEdit";
import {getComparator} from "../../../components/core/getComparator";
import {useSearchParams} from "react-router-dom";
import {Loading} from "../../../components/core/Loading";
import CloudUploadIcon from '@mui/icons-material/CloudUpload';
import {styled} from "@mui/material/styles";
const emptyCocktail = {
id: null,
name: "",
alcoholic: "",
category: "",
components: "",
glass: "",
image: "",
instructions: "",
isAllowed: false,
rating: {
rating: 0,
favourite: false
},
receipt: [],
tags: "",
video: ""
};
const alcohol = [
{
id: 1,
name: "Алкогольный"
},
{
id: 2,
name: "Безалкогольный",
},
{
id: 3,
name: "Опционально"
}
]
const VisuallyHiddenInput = styled('input')({
clip: 'rect(0 0 0 0)',
clipPath: 'inset(50%)',
height: 1,
overflow: 'hidden',
position: 'absolute',
bottom: 0,
left: 0,
whiteSpace: 'nowrap',
width: 1,
});
export function EditCocktailPage() {
const [searchParams] = useSearchParams();
const [loading, setLoading] = useState(true);
const {createError, createSuccess, getError} = useAlert();
const [cocktails, setCocktails] = useState([]);
const [selected, setSelected] = useState(null);
const [cocktail, setCocktail] = useState(emptyCocktail);
const [glass, setGlass] = useState([]);
const [category, setCategory] = useState([]);
const [tags, setTags] = useState([])
useEffect(() => {
api().get(requests.cocktails.simple)
.then((r) => {
const arr = r.data.sort(getComparator("asc", "name"));
setCocktails(arr)
const currentId = searchParams.get("id");
if (!currentId) {
setLoading(false);
return;
}
const currentCocktail = arr.find((r) => r.id === (currentId * 1));
if (!currentCocktail) {
setLoading(false);
return;
}
setSelected(currentCocktail.id);
setLoading(false);
})
.catch(() => createError("Ошибка получения данных"))
api().get(requests.bar.category)
.then((r) => setCategory(r.data.sort(getComparator("asc", "name"))))
.catch(() => createError("Ошибка получения категорий"))
api().get(requests.bar.glass)
.then((r) => setGlass(r.data.sort(getComparator("asc", "name"))))
.catch(() => createError("Ошибка получения посуды"))
api().get(requests.bar.tags)
.then((r) => setTags(r.data.sort(getComparator("asc", "name"))))
.catch(() => createError("Ошибка получения тегов"))
// eslint-disable-next-line
}, []);
useEffect(() => {
if (!selected) {
setCocktail(emptyCocktail);
return;
}
api().get(requests.cocktails.cocktail + selected)
.then((r) => {
setCocktail(r.data)
})
.catch(() => getError());
// eslint-disable-next-line
}, [selected])
const changeCocktailValue = (name, value) => {
if (name === "tags") {
value = value.join(",");
}
setCocktail((prev) => ({
...prev,
[name]: value
}))
}
const saveHandler = () => {
api().patch(requests.cocktails.edit, cocktail)
.then((r) => {
if (!r.data.error) {
createSuccess("Сохранено")
return;
}
createError("Ошибка на сервере: " + r.data.error)
})
.catch(() => createError("Неизвестная ошибка"))
}
const deleteHandle = () => {
api().delete(requests.cocktails.cocktail + cocktail.id)
.then(() => {
setCocktails(cocktails.filter((r) => r.id !== cocktail.id))
setCocktail(emptyCocktail);
})
.catch(() => createError("Ошибка удаления коктейля"))
}
return (
<Box>
{/*Загрузка*/}
<Loading loading={loading}/>
{/*Заголовок*/}
<Toolbar>
<Typography variant="h6" component="div" sx={{flexGrow: 1}}>Коктейли</Typography>
</Toolbar>
{/*Поиск*/}
<Paper elevation={6} sx={{my: 2, display: 'grid', p: 2}}>
<Autocomplete
disablePortal
options={cocktails}
onChange={(e, v) => {
if (!v) {
setCocktail(emptyCocktail);
setSelected(null)
} else {
setSelected(v.id)
}
}}
isOptionEqualToValue={(selected, value) => selected.id === value.id}
getOptionKey={(selected) => selected.id}
getOptionLabel={(selected) => selected.name + (selected.hasError ? " (есть ошибка)" : "")}
renderInput={(params) => <TextField {...params} label="Поиск"/>}
/>
</Paper>
{/*Рабочая область*/}
<Paper elevation={6} sx={{my: 2, display: 'grid', p: 1, pb: 2}}>
<Stack>
<Box hidden={cocktail.id === null} ml={1} mb={1}>
<Button color='error' onClick={() => deleteHandle()}>Удалить коктейль</Button>
</Box>
{/*Фото*/}
<Box ml={1}>
<img src={cocktail.image} alt={""} width={300} height={300} loading={'eager'}/>
</Box>
{/*Редактирование ссылки на фото*/}
<Stack direction='row' pr={2} m={1} display='relative'>
<TextField sx={{width: '75%'}}
label={"Ссылка на фото"} variant='outlined' multiline
value={!cocktail.image ? "" : cocktail.image}
onChange={(e) => changeCocktailValue("image", e.target.value)}
/>
<Button
component="label"
role={undefined}
variant="contained"
tabIndex={-1}
startIcon={<CloudUploadIcon/>}
sx={{width: '10%', fontSize: 40, ml: 1, pr: 1}}
>
<VisuallyHiddenInput
type="file"
accept=".jpg,.jpeg,.png"
onChange={(event) => {
const file = event.target.files[0];
let formData = new FormData();
formData.append('file', file);
api().post(requests.cocktails.savePhoto, formData)
.then((r) => changeCocktailValue("image", r.data))
.catch(() => getError())
}}
/>
</Button>
</Stack>
{/*Название*/}
<Box m={1}>
<TextField sx={{mr: 1, mb: 2, minWidth: 300}}
variant="outlined" label={"Название"}
value={cocktail.name}
onChange={(e) => changeCocktailValue("name", e.target.value)}/>
</Box>
{/*Категория, посуда, алкогольность, теги*/}
<Box mb={2}>
<SelectEdit value={cocktail.category} label={"Категория"} width={300} margin={1}
array={category}
attributeName={"category"} handler={changeCocktailValue}/>
<SelectEdit value={cocktail.glass} label={"Посуда"} width={300} margin={1} array={glass}
attributeName={"glass"} handler={changeCocktailValue}/>
<SelectEdit value={cocktail.alcoholic} label={"Алкогольность"} width={300} margin={1}
array={alcohol}
attributeName={"alcoholic"} handler={changeCocktailValue}/>
<CheckMarks rows={tags} width={300} name={"Теги"} handleChange={changeCocktailValue}
filterValue={cocktail.tags.split(",")} filterName={"tags"}/>
</Box>
{/*Рецепт*/}
<EditCocktailReceipt receipt={cocktail.receipt} handler={changeCocktailValue}/>
<Box pr={2} ml={1}>
<TextField sx={{width: '100%'}}
label={"Инструкция"} variant='outlined' multiline
value={!cocktail.instructions ? "" : cocktail.instructions}
onChange={(e) => changeCocktailValue("instructions", e.target.value)}
/>
</Box>
</Stack>
</Paper>
<Box display={'flex'} justifyContent={'flex-end'}>
<Button variant='contained' onClick={() => saveHandler()}>Сохранить</Button>
</Box>
</Box>
)
}

View File

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

View File

@@ -0,0 +1,169 @@
import Box from "@mui/material/Box";
import Toolbar from "@mui/material/Toolbar";
import Typography from "@mui/material/Typography";
import * as React from "react";
import {useEffect, useState} from "react";
import Paper from "@mui/material/Paper";
import {Autocomplete, FormControl, FormControlLabel, InputLabel} from "@mui/material";
import {api} from "../../../lib/clients/api";
import {requests} from "../../../requests";
import {useAlert} from "../../../hooks/useAlert";
import {useSearchParams} from "react-router-dom";
import TextField from "@mui/material/TextField";
import Switch from "@mui/material/Switch";
import Stack from "@mui/material/Stack";
import Button from "@mui/material/Button";
import Select from "@mui/material/Select";
import MenuItem from "@mui/material/MenuItem";
import {getComparator} from "../../../components/core/getComparator";
const emptyIngredient = {
id: null,
name: "",
enName: "",
have: false,
image: null,
type: "",
alcohol: false,
abv: null,
description: null
}
export function EditIngredientPage() {
const [searchParams] = useSearchParams();
const [ingredients, setIngredients] = useState([]);
const [types, setTypes] = useState([]);
const [ingredient, setIngredient] = useState(emptyIngredient)
const {createError, createSuccess} = useAlert();
useEffect(() => {
api().get(requests.bar.ingredientList)
.then((r) => {
const arr = r.data.sort(getComparator("asc", "name"));
setIngredients(arr)
const currentId = searchParams.get("id");
if (!currentId) {
return;
}
const currentIngredient = arr.find((r) => r.id === (currentId * 1));
if (!currentIngredient) {
return;
}
setIngredient(currentIngredient);
})
.catch(() => createError("Ошибка получения данных"))
api().get(requests.bar.type)
.then((r) => setTypes(r.data.sort(getComparator("asc", "name"))))
// eslint-disable-next-line
}, []);
const changeIngredientValue = (name, value) => {
setIngredient((prev) => ({
...prev,
[name]: value
}))
}
const saveIngredientHandler = () => {
api().patch(requests.bar.ingredient, ingredient)
.then(() => createSuccess("Ингредиент сохранен"))
.catch(() => createError("Ошибка сохранения"))
}
return (
<Box>
{/*Заголовок*/}
<Toolbar>
<Typography variant="h6" component="div" sx={{flexGrow: 1}}>Ингредиенты</Typography>
</Toolbar>
{/*Поиск*/}
<Paper elevation={6} sx={{my: 2, display: 'grid', p: 2}}>
<Autocomplete
disablePortal
options={ingredients}
defaultChecked={emptyIngredient}
onChange={(e, v) => {
console.log(v);
return !v ? setIngredient(emptyIngredient) : setIngredient(v)
}}
isOptionEqualToValue={(selected, value) => selected.id === value.id}
getOptionKey={(selected) => selected.id}
getOptionLabel={(selected) => selected.name}
renderInput={(params) => <TextField {...params} label="Ингредиенты"/>}
/>
</Paper>
{/*Форма ингредиента*/}
<Paper elevation={6} sx={{my: 2, display: 'grid', p: 1, pb: 2}}>
<Stack>
<Box display={'flex'} justifyContent={'flex-end'} pr={2}>
<FormControlLabel control={
<Switch
checked={ingredient.have}
onChange={() => changeIngredientValue("have", !ingredient.have)}
/>}
label={"Наличие"} labelPlacement={'start'}/>
</Box>
<Box>
<img src={ingredient.image} alt={""} loading={'eager'}/>
</Box>
<Box m={1}>
<TextField sx={{mr: 1, mb: 2, minWidth: 330}}
variant="outlined" label={"Название"}
value={ingredient.name}
onChange={(e) => changeIngredientValue("name", e.target.value)}/>
<TextField sx={{mr: 1, mb: 2, minWidth: 330}}
label="Английское название" variant="outlined"
value={ingredient.enName}
onChange={(e) => changeIngredientValue("enName", e.target.value)}/>
</Box>
<Box height={70} mt={1} ml={1}>
<FormControlLabel sx={{pt: 1}}
control={
<Switch
checked={ingredient.alcohol}
onChange={() => changeIngredientValue("alcohol", !ingredient.alcohol)}
/>}
label="Алкогольный"/>
{ingredient.alcohol && (
<TextField sx={{width: 100}}
variant='outlined' label='Градус'
value={!ingredient.abv ? "" : ingredient.abv}
onChange={(e) => changeIngredientValue("abv", e.target.value)}/>
)}
</Box>
<Box mb={2} ml={1}>
<FormControl sx={{width: 330}}>
<InputLabel id="select-label">Категория</InputLabel>
<Select
id="select-label"
autoWidth
label={"Категория"}
value={!ingredient.type ? "" : ingredient.type}
onChange={(e) => changeIngredientValue("type", e.target.value)}
>
<MenuItem value="">
<em>None</em>
</MenuItem>
{types.map((c) => {
return (<MenuItem key={c.id} value={c.name}>{c.name}</MenuItem>)
})}
</Select>
</FormControl>
</Box>
<Box pr={2} ml={1}>
<TextField sx={{width: '100%'}}
label={"Описание"} variant='outlined' multiline
value={!ingredient.description ? "" : ingredient.description}/>
</Box>
</Stack>
</Paper>
<Box display={'flex'} justifyContent={'flex-end'}>
<Button variant='contained' onClick={() => saveIngredientHandler()}>Сохранить</Button>
</Box>
</Box>
)
}

View File

@@ -0,0 +1,153 @@
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import Toolbar from "@mui/material/Toolbar";
import Paper from "@mui/material/Paper";
import {Fab, FormControl, FormControlLabel, InputAdornment, InputLabel, OutlinedInput, Tabs} from "@mui/material";
import IconButton from "@mui/material/IconButton";
import SearchIcon from "@mui/icons-material/Search";
import * as React from "react";
import {useEffect, useMemo, useState} from "react";
import {Loading} from "../../../components/core/Loading";
import {requests} from "../../../requests";
import {useAlert} from "../../../hooks/useAlert";
import {IngredientInfoModal} from "../../../components/Ingredients/IngredientInfoModal";
import {api} from "../../../lib/clients/api";
import Tab from "@mui/material/Tab";
import {a11yProps} from "../../../components/core/tabProps";
import {CustomTabPanel} from "../../../components/core/TabPanel";
import {IngredientList} from "../../../components/Ingredients/IngredientList";
import {blue} from "@mui/material/colors";
import UpIcon from "@mui/icons-material/KeyboardArrowUp";
import Switch from "@mui/material/Switch";
export function IngredientsPage() {
const [value, setValue] = React.useState(0);
const [grouping, setGrouping] = useState(true);
const handleChange = (event, newValue) => setValue(newValue);
const [loading, setLoading] = useState(true);
const [findString, setFindString] = useState("");
const [ingredients, setIngredients] = useState([]);
const [openModal, setOpenModal] = useState(false);
const [selectedInfo, setSelectedInfo] = useState(null);
const {createError} = useAlert();
useEffect(() => {
api().get(requests.bar.ingredientList)
.then((r) => {
setIngredients(r.data)
setLoading(false);
})
.catch(() => {
createError("Ошибка получения списка ингредиентов");
setLoading(false);
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const visibleIngredients = useMemo(() => {
if (findString.length === 0) {
return ingredients;
}
const reg = new RegExp("(.*?)" + findString + "(.*?)", "i");
return ingredients.filter((ingredient) => ingredient.name.match(reg) !== null);
}, [findString, ingredients]);
const ingredientsToAdd = visibleIngredients.filter((ingredient) => !ingredient.have);
const ingredientsInBar = visibleIngredients.filter((ingredient) => ingredient.have);
const changeHandler = (row, value) => {
const newState = ingredients.map((ingredient) => {
if (ingredient.id === row.id) {
return {
...ingredient,
have: value
}
} else {
return ingredient;
}
})
const url = `${requests.bar.ingredient}?id=${row.id}`;
const request = value ? api().put(url) : api().delete(url);
request
.then(() => {
setIngredients(newState);
})
.catch(() => {
createError("Ошибка изменения ингредиента");
});
}
const handleOpenModal = (i) => {
setOpenModal(true);
setSelectedInfo(i);
}
const handleCloseModal = () => {
setSelectedInfo(null);
setOpenModal(false);
}
return (
<Box>
{/*Заголовок*/}
<Toolbar>
<Typography variant="h6" component="div" sx={{flexGrow: 1}}>Ингредиенты бара</Typography>
</Toolbar>
{/*Поиск*/}
<Paper elevation={6} sx={{my: 2}}>
<FormControl sx={{m: 1, width: 'calc(100% - 20px'}}>
<InputLabel htmlFor="outlined-adornment-amount">Поиск</InputLabel>
<OutlinedInput
onChange={(e) => setFindString(e.target.value)}
label="With normal TextField"
startAdornment={
<InputAdornment position="start">
<IconButton edge="end">
<SearchIcon/>
</IconButton>
</InputAdornment>
}
/>
</FormControl>
<FormControlLabel sx={{ml: '2px'}}
control={<Switch defaultChecked/>}
onClick={() => setGrouping(!grouping)}
label="Группировать"
labelPlacement="end"/>
</Paper>
{/*Рабочее поле ингредиентов*/}
<Box>
<Tabs value={value} onChange={handleChange} aria-label="basic tabs example">
<Tab label="В баре" {...a11yProps(0)} />
<Tab label="Список" {...a11yProps(1)} />
</Tabs>
</Box>
<Box>
<CustomTabPanel value={value} index={0}>
<IngredientList rows={ingredientsInBar} value={false} changeHandler={changeHandler}
infoHandler={handleOpenModal} grouping={grouping}/>
</CustomTabPanel>
<CustomTabPanel value={value} index={1}>
<IngredientList rows={ingredientsToAdd} value={true} changeHandler={changeHandler}
infoHandler={handleOpenModal} grouping={grouping}/>
</CustomTabPanel>
</Box>
<Fab sx={{
alpha: '30%',
position: 'sticky',
bottom: '16px',
color: 'common.white',
bgcolor: blue[600],
'&:hover': {
bgcolor: blue[600],
},
}}
onClick={() => window.window.scrollTo(0, 0)}
aria-label='Expand'
color='inherit'>
<UpIcon/>
</Fab>
{/*Загрузчик*/}
<Loading loading={loading}/>
{/*Модальное окно информации об ингредиенте*/}
<IngredientInfoModal ingredient={selectedInfo} open={openModal} closeHandler={handleCloseModal}/>
</Box>
)
}

View File

@@ -0,0 +1,38 @@
import * as React from 'react';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft';
import {paths} from "../../../path";
export default function NotFoundPage() {
return (
<Box component="main" sx={{ alignItems: 'center', display: 'flex', justifyContent: 'center', minHeight: '100%' }}>
<Stack spacing={3} sx={{ alignItems: 'center', maxWidth: 'md' }}>
<Box>
<Box
component="img"
alt="Under development"
src="/assets/error-404.png"
sx={{ display: 'inline-block', height: 'auto', maxWidth: '100%', width: '400px' }}
/>
</Box>
<Typography variant="h3" sx={{ textAlign: 'center' }}>
404: Страница не найдена или недоступна
</Typography>
<Typography color="text.secondary" variant="body1" sx={{ textAlign: 'center' }}>
Вы либо выбрали какой-то сомнительный маршрут, либо попали сюда по ошибке. Что бы это ни было, попробуйте воспользоваться навигацией
</Typography>
<Button
// component={'a'}
href={paths.home}
startIcon={<ArrowLeftIcon fontSize="var(--icon-fontSize-md)" />}
variant="contained"
>
На домашнюю страницу
</Button>
</Stack>
</Box>
);
}

View File

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

View File

@@ -0,0 +1,82 @@
import {useEffect, useMemo, useState} from "react";
import {useAlert} from "../../../hooks/useAlert";
import * as React from "react";
import {api} from "../../../lib/clients/api";
import {requests} from "../../../requests";
import {createHeadCell} from "../../../components/orders/createHeadCelll";
import {Loading} from "../../../components/core/Loading";
import OrderModal from "../../../components/orders/OrderModal";
import EnhancedTable from "../../../components/orders/EnhancedTable";
export function QueueContent({my}) {
const [load, setLoad] = useState(false);
const [orders, setOrders] = useState([]);
const {createSuccess, createError} = useAlert();
const [openModal, setOpenModal] = React.useState(false);
const [selected, setSelected] = useState(null);
useEffect(() => {
setLoad(false);
const url = my ? requests.bar.myOrders : requests.bar.order;
api().get(url)
.then(r => {
setOrders(r.data);
setLoad(true);
})
.catch(() => {
createError("Ошибка при получении заказов");
setLoad(true)
})
// eslint-disable-next-line
}, []);
const sliced = useMemo(() => orders.sort((a, b) => b.id - a.id), [orders])
const cells = [
createHeadCell('id', true, true, 'Номер заказа', "20px"),
createHeadCell('cocktail.name', true, false, 'Коктейль', "40px"),
createHeadCell('visitor.name', true, false, 'Клиент', "40px"),
createHeadCell('status', true, true, 'Статус', "30px"),
];
const changeOrderHandle = (row, status) => {
let url = requests.bar.order + "?id=" + row.id;
let isCancel = status === "CANCEL";
let request = isCancel ? api().delete(url) : api().put(url);
request
.then(() => {
createSuccess(isCancel ? "Заказ отменен" : "Заказ готов");
let newArr = orders.filter((order) => {
if (order.id !== row.id) {
row.status = isCancel ? "CANCEL" : "DONE";
return row;
}
return order;
})
setOrders(newArr);
setSelected(null);
setOpenModal(false);
})
.catch(() => createError("Ошибка изменения заказа"))
}
const handleSelect = (row) => {
setSelected(row);
setOpenModal(true);
}
const handleCloseModal = () => {
setOpenModal(false);
setSelected(null);
};
const filterValues = !my ? ["DONE", "CANCEL"] : [];
return (
<>
<Loading loading={!load}/>
<OrderModal row={selected} handleClose={handleCloseModal} open={openModal}
handleChange={changeOrderHandle} my={my}/>
<EnhancedTable name={"Заказы"} rows={sliced} cells={cells} handleSelect={handleSelect} filterEqual={false}
filterField={["status"]} filterValue={filterValues}/>
</>
)
}

View File

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

View File

@@ -0,0 +1,37 @@
import {Card} from "@mui/material";
import IconButton from "@mui/material/IconButton";
import DeleteIcon from '@mui/icons-material/Delete';
import AddBoxRoundedIcon from '@mui/icons-material/AddBoxRounded';
import InfoRoundedIcon from '@mui/icons-material/InfoRounded';
import React from "react";
import Box from "@mui/material/Box";
import Stack from "@mui/material/Stack";
import {paths} from "../../path";
import EditIcon from '@mui/icons-material/Edit';
export function IngredientCard({row, value, infoHandler, changeHandler}) {
return (
<Card sx={{mb: 1, height: '130px', display: 'relative', pt: 1}}>
<Stack direction='row' justifyContent='start' alignItems='center'>
<Box sx={{width: '100px', height: '100px'}}>
<img src={row.image} loading='eager' height={'100px'} width={'100px'} alt={row.id}/>
</Box>
<Box sx={{width: 'calc(90% - 100px)', pr: 2}}>{row.name}</Box>
<Stack direction='row'>
<Box mr={1} pt={'3px'}>{!row.alcohol ? "" : `${row.abv}%`}</Box>
<Stack sx={{width: '5%'}} spacing={1} justifyContent='flex-start'>
<IconButton size='small' onClick={() => changeHandler(row, value)}>
{value ? <AddBoxRoundedIcon/> : <DeleteIcon/>}
</IconButton>
<IconButton size='small' onClick={() => infoHandler(row)}>
<InfoRoundedIcon/>
</IconButton>
<IconButton size='small' href={`${paths.bar.ingredientEdit}?id=${row.id}`}>
<EditIcon/>
</IconButton>
</Stack>
</Stack>
</Stack>
</Card>
)
}

View File

@@ -0,0 +1,37 @@
import Dialog from "@mui/material/Dialog";
import DialogTitle from "@mui/material/DialogTitle";
import DialogContent from "@mui/material/DialogContent";
import DialogActions from "@mui/material/DialogActions";
import Button from "@mui/material/Button";
import * as React from "react";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
export function IngredientInfoModal({ingredient, open, closeHandler}) {
if (!ingredient) {
return null;
}
return (
<Dialog fullWidth={true} maxWidth="350px" open={open} onClose={closeHandler}
sx={{
'& .MuiDialog-paper': {
margin: '8px',
},
'& .MuiPaper-root': {
width: 'calc(100% - 16px)',
}
}}>
<DialogTitle>{ingredient.name}</DialogTitle>
<DialogContent>
<Stack spacing={2} sx={{justifyContent: "center"}}>
<img src={ingredient.image} alt={ingredient.name} loading={"eager"} width={"300"}/>
{ingredient.alcohol && (<Typography>{`Крепость ${ingredient.abv}`}</Typography>)}
<Typography>{ingredient.description}</Typography>
</Stack>
</DialogContent>
<DialogActions>
<Button onClick={closeHandler}>Close</Button>
</DialogActions>
</Dialog>
);
}

View File

@@ -0,0 +1,75 @@
import Box from "@mui/material/Box";
import {IngredientCard} from "./IngredientCard";
import {useMemo, useState} from "react";
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import {Accordion, AccordionDetails, AccordionSummary} from "@mui/material";
import Typography from "@mui/material/Typography";
import {getComparator} from "../core/getComparator";
import {groupByForLoop} from "../core/groupByForLoop";
export function IngredientList({rows, value, infoHandler, changeHandler, grouping}) {
const [size, setSize] = useState(10);
window.addEventListener('scroll', () => {
if (window.innerHeight + window.scrollY >= (document.documentElement.scrollHeight - 100)) {
if (!grouping) {
setSize(size + 10)
}
}
});
const visibleRows = useMemo(() => {
let res = [];
if (rows.length === 0) {
return null;
}
if (!grouping) {
return rows
.sort(getComparator("asc", "name"))
.slice(0, size)
.map((row) => {
return (
<IngredientCard row={row} key={row.id} value={value}
changeHandler={changeHandler} infoHandler={infoHandler}/>
)
})
}
const group = groupByForLoop(rows, "type")
if (!group || group.size === 0) {
return null;
}
const keys = Array.from(group.keys());
keys.sort(getComparator("asc"))
.forEach((key) => {
const list = group.get(key)
res.push(
<Accordion key={key}>
<AccordionSummary
expandIcon={<ExpandMoreIcon/>}
aria-controls="panel1-content"
id="panel1-header"
>
<Typography component="span">{key !== "null" ? key : "Без категории"}</Typography>
</AccordionSummary>
<AccordionDetails>
{list.sort(getComparator("asc", "name"))
.map((row) => {
return (
<IngredientCard row={row} key={row.id} value={value}
changeHandler={changeHandler} infoHandler={infoHandler}/>
)
})}
</AccordionDetails>
</Accordion>
)
})
return res;
// eslint-disable-next-line
}, [size, rows])
return (
<Box mt={2}>
{visibleRows}
</Box>
)
}

View File

@@ -0,0 +1,48 @@
'use client';
import * as React from 'react';
import Alert from '@mui/material/Alert';
import {useAuth} from "../../hooks/useAuth";
import {logger} from "../../lib/DefaultLogger";
import {paths} from "../../path";
export function GuestGuard({ children }) {
const { auth, error, isLoading } = useAuth();
const [isChecking, setIsChecking] = React.useState(true);
const checkPermissions = async () => {
if (isLoading) {
return;
}
if (error) {
setIsChecking(false);
return;
}
if (auth) {
logger.debug('[GuestGuard]: User is logged in, redirecting to dashboard');
window.location.replace(paths.dashboard.overview);
return;
}
setIsChecking(false);
};
React.useEffect(() => {
checkPermissions().catch(() => {
// noop
});
// eslint-disable-next-line react-hooks/exhaustive-deps -- Expected
}, [auth, error, isLoading]);
if (isChecking) {
return null;
}
if (error) {
return <Alert color="error">{error}</Alert>;
}
return <React.Fragment>{children}</React.Fragment>;
}

View File

@@ -0,0 +1,135 @@
import * as React from 'react';
import {useState} from 'react';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import {paths} from "../../path";
import TextField from "@mui/material/TextField";
import TelegramIcon from '@mui/icons-material/Telegram';
import IconButton from "@mui/material/IconButton";
import Button from "@mui/material/Button";
import CircularProgress from "@mui/material/CircularProgress";
import Box from "@mui/material/Box";
import {red} from "@mui/material/colors";
import {requests} from "../../requests";
import {useAuth} from "../../hooks/useAuth";
import {api} from "../../lib/clients/api";
const emptyRequest = {
byLogin: false,
code: "",
login: "",
password: ""
}
export function SignInForm() {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [request, setRequest] = useState(emptyRequest);
const [pass, setPass] = useState(false)
const {checkSession} = useAuth();
const buttonSx = {
minWidth: "300px",
...(error && {
bgcolor: red[500],
'&:hover': {
bgcolor: red[700],
},
}),
};
const handleButtonClick = async () => {
setLoading(true);
const response = await api().post(requests.auth.login, request);
if (response.data.error) {
setError(response.data.error);
setLoading(false);
return;
}
localStorage.setItem("token", response.data.token);
await checkSession?.();
window.location.reload();
}
const renderByCode = () => {
return (
<Stack direction='row' mt={1}>
<IconButton href={paths.auth.bot} target="_blank" color='primary'>
<TelegramIcon/>
</IconButton>
<TextField value={request.code}
onChange={(e) => setRequest((prevState) => ({
...prevState,
code: e.target.value
}))}
sx={{minWidth: 300}} id="outlined-basic" label="Код подтверждения"
variant="outlined"/>
</Stack>
)
}
const renderByLogin = () => {
return (
<Stack mt={1} spacing={2}>
<TextField value={request.login}
onChange={(e) => setRequest(prevState => ({
...prevState,
login: e.target.value
}))}
sx={{minWidth: 300}} id="loginField" label="Логин"
variant="outlined"/>
<TextField value={request.password}
onChange={(e) => setRequest((prevState) => ({
...prevState,
password: e.target.value
}))}
sx={{minWidth: 300}} id="passwordField" label="Пароль" type="password"
autoComplete="current-password" variant="outlined"/>
</Stack>
)
}
return (
<Stack spacing={4} sx={{marginBottom: '85%', marginTop: '45%'}}>
<Stack spacing={1}>
<Typography variant="h4">Авторизация</Typography>
<Typography variant='body1' component="a" href='#'
onClick={() => {
setRequest((prevState) => ({
...prevState,
byLogin: !pass
}))
setPass(!pass)
}}>
{pass ? "Вход по телеграмм-коду" : "Вход по логину и паролю"}
</Typography>
<Typography color="text.secondary" variant="body2">
{pass ? "Введите логин и пароль"
: "Для входа нужно всего лишь сказать об этом моему Telegram-боту, перейди по ссылке и набери \n/start"}
</Typography>
{pass ? renderByLogin() : renderByCode()}
<Box sx={{display: 'flex', alignItems: 'center', ml: '40px', mt: 1}}>
<Stack>
{error && (
<Typography mb={1} color={'error'}>{error}</Typography>
)}
<Button
variant="contained"
sx={buttonSx}
disabled={loading}
onClick={handleButtonClick}
>
{loading ? (
<CircularProgress
size={25}
/>
) : "Войти"}
</Button>
</Stack>
</Box>
</Stack>
</Stack>
);
}

View File

@@ -0,0 +1,40 @@
import {Card} from "@mui/material";
import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box";
import React from "react";
import Typography from "@mui/material/Typography";
import Button from "@mui/material/Button";
const role = (myRole) => {
switch (myRole) {
case "ADMIN":
return "Администратор";
case "ADMIN_NOT_BARMEN":
return "Управляющий";
default:
return null;
}
}
export function BarItem({row, changeHandler, all, enterExist}) {
return (
<Card sx={{mb: 1, height: '100px', display: 'relative', p: 1}}>
<Stack direction='row' justifyContent='start' alignItems='start'>
<Box sx={{width: '75%', pr: 2}}>
<Typography variant='h6'>{row.name}</Typography>
<Typography>{role(row.myRole)}</Typography>
</Box>
<Stack sx={{width: '25%'}} spacing={1} justifyContent='flex-end' display='flex'>
<Typography color={row.open ? 'green' : 'red'}>{row.open ? "Бар открыт" : "Бар закрыт"}</Typography>
<Button variant='contained'
color={row.enter ? 'error' : 'success'}
onClick={() => changeHandler(row, !row.enter)}
disabled={!row.enter && enterExist}
>
{all ? "Добавить" : row.enter ? "Выйти" : "Войти"}
</Button>
</Stack>
</Stack>
</Card>
)
}

View File

@@ -0,0 +1,70 @@
import Box from "@mui/material/Box";
import {useEffect, useMemo, useState} from "react";
import {api} from "../../lib/clients/api";
import {requests} from "../../requests";
import {useAlert} from "../../hooks/useAlert";
import {BarItem} from "./BarItem";
import {Loading} from "../core/Loading";
import * as React from "react";
import {useUser} from "../../hooks/useUser";
export function BarList({all}) {
const {getError, createError} = useAlert();
const {refresh} = useUser();
const [bars, setBars] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
setLoading(true);
api().get(`${requests.bar.list}?my=${!all}`)
.then((r) => {
setBars(r.data)
setLoading(false);
})
.catch(() => getError())
// eslint-disable-next-line
}, []);
const enterExist = useMemo(() => bars.find((b) => b.enter), [bars])
const changeHandler = (row, value) => {
let request;
let newState;
if (!all) {
if (value && enterExist) {
//todo: добавить переключение
createError("Нельзя войти более чем в один бар одновременно")
return;
}
request = api().patch(`${requests.bar.enter}${row.id}&value=${value}`);
newState = bars.map((b) => {
if (b.id !== row.id) {
return b;
}
return {
...b,
enter: value
}
})
} else {
request = api().post(requests.bar.addToMyList, row);
newState = bars.filter((b) => b.id !== row.id);
}
request.then(() => {
setBars(newState)
refresh();
}).catch(() => getError())
}
return (
<Box mt={2}>
{
bars.map((row) => {
return (
<BarItem key={row.id} row={row} changeHandler={changeHandler}
all={all} enterExist={enterExist}/>
)
})
}
{/*Загрузчик*/}
<Loading loading={loading}/>
</Box>
)
}

View File

@@ -0,0 +1,68 @@
import * as React from 'react';
import OutlinedInput from '@mui/material/OutlinedInput';
import InputLabel from '@mui/material/InputLabel';
import MenuItem from '@mui/material/MenuItem';
import FormControl from '@mui/material/FormControl';
import ListItemText from '@mui/material/ListItemText';
import Select from '@mui/material/Select';
import Checkbox from '@mui/material/Checkbox';
const ITEM_HEIGHT = 48;
const ITEM_PADDING_TOP = 8;
const MenuProps = {
PaperProps: {
style: {
maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP,
width: 250,
},
},
};
export default function CheckMarks({rows, name, filterValue, handleChange, filterName, width, nonMulti, nullValue, identity}) {
const realValue = !nonMulti ? filterValue.filter((v) => v.length > 0) : filterValue;
return (
<div>
<FormControl sx={{m: 1, width: !width ? 300 : width}}>
<InputLabel>{name}</InputLabel>
<Select
multiple={!nonMulti}
value={realValue}
onChange={(e) => handleChange(filterName, e.target.value)}
input={<OutlinedInput label={name}/>}
renderValue={(selected) => !nonMulti ? selected.join(", ") : selected}
MenuProps={MenuProps}
defaultChecked={nonMulti && rows[0]}
variant="filled">
{(nonMulti && nullValue) && (
<MenuItem value={""}>
<em>Не выбрано</em>
</MenuItem>
)}
{rows.map((value) => {
if(identity) {
return (
<MenuItem key={"menuItemIn" + value} value={value}>
{!nonMulti && (
<Checkbox
checked={realValue.includes(value)}/>
)}
<ListItemText primary={value}/>
</MenuItem>
)
} else {
return (
<MenuItem key={value.id} value={value.name}>
{!nonMulti && (
<Checkbox
checked={realValue.includes(value.name)}/>
)}
<ListItemText primary={value.name}/>
</MenuItem>
)
}
})}
</Select>
</FormControl>
</div>
);
}

View File

@@ -0,0 +1,113 @@
import {CardActions, CardContent, CardMedia, Rating} from "@mui/material";
import {useAlert} from "../../hooks/useAlert";
import Typography from "@mui/material/Typography";
import List from "@mui/material/List";
import ListItem from "@mui/material/ListItem";
import ListItemText from "@mui/material/ListItemText";
import Button from "@mui/material/Button";
import Grid from "@mui/material/Grid";
import {requests} from "../../requests";
import {CocktailItemStyled} from "./CocktailItemStyled";
import IconButton from "@mui/material/IconButton";
import FavoriteBorderIcon from '@mui/icons-material/FavoriteTwoTone';
import FavoriteIcon from '@mui/icons-material/Favorite';
import {api} from "../../lib/clients/api";
import Box from "@mui/material/Box";
import {useUser} from "../../hooks/useUser";
function renderFavouriteBadge(handleFavourite, row) {
const childIcon = row.rating.favourite ? <FavoriteIcon color='error'/> : <FavoriteBorderIcon color={'warning'}/>;
return (
<IconButton sx={{position: 'absolute', top: "15px", right: "15px"}} onClick={() => handleFavourite(row)}>
{childIcon}
</IconButton>
)
}
function renderRating(handleChangeRating, row) {
return (
<Rating
sx={{position: 'absolute', top: '310px', right: '85px'}}
name="simple-controlled"
size="large"
value={row.rating.rating}
onChange={(event, newValue) => handleChangeRating(row, newValue)}
/>
)
}
export function Cocktail({row, handleFavourite, handleChangeRating, handleSelect, editMenuBlock}) {
const {createAlert, createError} = useAlert();
const {session, user} = useUser();
function pay(cocktailId) {
api().post(`${requests.bar.pay}cocktail=${cocktailId}`)
.then(() => createAlert("Ожидайте свой заказ", "success"))
.catch(() => createError("Ошибка во время создания заказа"))
}
return (
<Grid item sx={{pr: 2}}>
<CocktailItemStyled>
<Box sx={{
p: '4px 4px',
m: 1,
width: '320px',
position: 'relative',
}}>
<CardMedia
sx={{
loading: "eager",
borderRadius: 2
}}
onClick={() => handleSelect(row)}
component="img"
alt={row.name}
height="300"
image={`${row.image}/preview`}
/>
{renderFavouriteBadge(handleFavourite, row)}
{renderRating(handleChangeRating, row)}
<CardContent sx={{pb: '4px', pl: 2}}>
<Typography variant="h5" minHeight={'50px'} mt={2}>{row.name} </Typography>
<List sx={{py: '0px'}}>
{row.hasError && (
<ListItem sx={{p: '2px 12px 0px 0px', m: '0px'}}>
<ListItemText color={'red'}>Имеет ошибку в рецепте или ингредиентах</ListItemText>
</ListItem>
)}
<ListItem sx={{p: '2px 12px 0px 0px', m: '0px'}}>
<ListItemText>{"Категория: " + row.category}</ListItemText>
</ListItem>
<ListItem sx={{p: '2px 12px 0px 0px', m: '0px'}}>
<ListItemText>{"Алкоголь: " + row.alcoholic}</ListItemText>
</ListItem>
{row.volume !== null && (
<ListItem sx={{p: '2px 12px 0px 0px', m: '0px'}}>
<ListItemText>{"Крепость: ≈" + row.volume}</ListItemText>
</ListItem>
)}
<ListItem sx={{p: '2px 12px 0px 0px', m: '0px'}}>
<ListItemText>{"Подача: " + row.glass}</ListItemText>
</ListItem>
<ListItem sx={{p: '2px 12px 0px 0px', m: '0px'}}>
<ListItemText>{"Состав: " + row.components}</ListItemText>
</ListItem>
{(row.tags && row.tags.length > 0) && (
<ListItem sx={{p: '2px 12px 0px 0px', m: '0px'}}>
<ListItemText>{"Теги: " + row.tags.replaceAll(',', ', ')}</ListItemText>
</ListItem>)}
</List>
</CardContent>
<CardActions>
{(row.isAllowed && session.isActive && user.invited) &&
<Button variant="contained" onClick={() => pay(row.id)}>Заказать</Button>
}
{editMenuBlock(row)}
</CardActions>
</Box>
</CocktailItemStyled>
</Grid>
)
}

View File

@@ -0,0 +1,180 @@
import DialogTitle from "@mui/material/DialogTitle";
import DialogContent from "@mui/material/DialogContent";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import DialogActions from "@mui/material/DialogActions";
import Button from "@mui/material/Button";
import Dialog from "@mui/material/Dialog";
import * as React from "react";
import {useEffect, useState} from "react";
import {CardMedia} from "@mui/material";
import Paper from "@mui/material/Paper";
import Box from "@mui/material/Box";
import StarBorderIcon from '@mui/icons-material/StarBorder';
import IconButton from "@mui/material/IconButton";
import ShoppingCartIcon from '@mui/icons-material/ShoppingCart';
import DeleteIcon from '@mui/icons-material/Delete';
import {IngredientInfoModal} from "../Ingredients/IngredientInfoModal";
import {api} from "../../lib/clients/api";
import {requests} from "../../requests";
import {useAlert} from "../../hooks/useAlert";
import {paths} from "../../path";
import {Loading} from "../core/Loading";
import {useUser} from "../../hooks/useUser";
export function CocktailInfoModal({open, row, closeHandler}) {
const {user} = useUser();
const {getError, createError, createSuccess} = useAlert();
const [cocktail, setCocktail] = useState(null)
const [loading, setLoading] = useState(false);
const [selectedIngredient, setSelectedIngredient] = useState(null);
const [openIngredientModal, setOpenIngredientModal] = useState(false)
const closeIngredientHandler = () => {
setOpenIngredientModal(false);
setSelectedIngredient(null);
}
const openIngredientModalHandler = (id) => {
api().get(`${requests.bar.ingredient}?id=${id}`)
.then((r) => {
setSelectedIngredient(r.data)
setOpenIngredientModal(true);
}).catch(() => createError("Ошибка получения информации об ингредиенте"))
}
const selectIngredientHandler = (ingredient) => {
const url = `${requests.bar.ingredient}?id=${ingredient.id}`;
const request = ingredient.isHave ? api().delete(url) : api().put(url);
const value = !ingredient.isHave;
request.then(() => {
const newReceipts = cocktail.receipt.map((r) => {
if (r.ingredient.id !== ingredient.id) {
return r;
}
return {
...r,
ingredient: {
...ingredient,
isHave: value
}
}
})
setCocktail({
...cocktail,
receipt: newReceipts
})
createSuccess("Сохранено")
}).catch(() => createError("Ошибка сохранения"))
}
useEffect(() => {
setLoading(true)
if (!row) {
setLoading(false)
return;
}
api().get(requests.cocktails.modal + row)
.then((r) => {
setCocktail(r.data)
setLoading(false)
})
.catch(() => {
getError();
setLoading(false)
closeHandler();
})
}, [row]);
if (!row || !cocktail) {
return null;
}
let alko = 0;
let volume = 0;
return (
<Dialog fullWidth={true}
open={open} onClose={closeHandler}
sx={{
'& .MuiDialog-paper': {
margin: '8px',
},
'& .MuiPaper-root': {
width: 'calc(100% - 16px)',
}
}}>
<IngredientInfoModal ingredient={selectedIngredient} open={openIngredientModal}
closeHandler={closeIngredientHandler}/>
<Loading loading={loading}/>
<DialogTitle>
<Stack direction='row' justifyContent={'space-between'}>
<Box>{cocktail.name}</Box>
{cocktail.rating.rating > 0 &&
(
<Stack ml={3} direction='row'>
{`${cocktail.rating.rating}/5`}
<StarBorderIcon sx={{pb: "2px"}}/>
</Stack>
)
}
</Stack>
</DialogTitle>
<DialogContent>
<CardMedia
image={`${cocktail.image}/preview`}
sx={{
loading: "eager",
borderRadius: 2
}}
component="img"
alt={cocktail.name}
height="300"
/>
<Box mt={1}>
<Typography>Ингредиенты:</Typography>
<Paper sx={{p: 1}} elevation={3}>
<Stack>
{cocktail.receipt.map((r) => {
const hasError = r.count === null || r.unit === null;
const measure = hasError ? r.measure : (r.count + " " + r.unit.name)
if(alko !== null && volume !== null) {
console.log(r)
}
return (
<Stack key={r.ingredient.id} direction='row' justifyContent={'space-between'}
mt={1}>
<Stack direction='row'>
{user.role !== "USER" && (
<IconButton size="small" sx={{pb: "2px"}}
onClick={() => selectIngredientHandler(r.ingredient)}>
{r.ingredient.isHave
? (<DeleteIcon fontSize="small"/>)
: (<ShoppingCartIcon fontSize="small"/>)
}
</IconButton>
)}
<Typography
onClick={() => openIngredientModalHandler(r.ingredient.id)}>{r.ingredient.name}</Typography>
</Stack>
<Typography color={hasError && 'red'}>{measure}</Typography>
</Stack>
)
})}
</Stack>
</Paper>
</Box>
<Box>
<Typography mt={2}>Инструкция:</Typography>
<Paper sx={{p: 1}} elevation={3}>
<Box>
{cocktail.instructions}
</Box>
</Paper>
</Box>
</DialogContent>
<DialogActions>
{user.role.includes("ADMIN") && (
<Button href={`${paths.bar.cocktailEdit}?id=${cocktail.id}`}>Редактировать</Button>
)}
<Button onClick={closeHandler}>Закрыть</Button>
</DialogActions>
</Dialog>
)
}

View File

@@ -0,0 +1,12 @@
import {styled} from "@mui/material/styles";
import {Card} from "@mui/material";
export const CocktailItemStyled = styled(Card)(({theme}) => ({
backgroundColor: '#fff',
...theme.typography.body2,
padding: theme.spacing(1),
color: theme.palette.text.secondary,
...theme.applyStyles('dark', {
backgroundColor: '#1A2027',
})
}));

View File

@@ -0,0 +1,44 @@
import {Card, FormControlLabel} from "@mui/material";
import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box";
import IconButton from "@mui/material/IconButton";
import InfoRoundedIcon from "@mui/icons-material/InfoRounded";
import {paths} from "../../path";
import EditIcon from "@mui/icons-material/Edit";
import React from "react";
import Switch from "@mui/material/Switch";
export function CocktailListCard({row, changeHandler, infoHandler}) {
return (
<Card sx={{mb: 1, height: '130px', display: 'relative', pt: 1, borderRadius: '15px'}}>
<Stack direction='row' justifyContent='start' alignItems='start'>
<Box sx={{width: '100px', height: '100px', ml: 1}}>
<img src={row.image} loading='eager' height={'100px'} width={'100px'} alt={row.id}
style={{borderRadius: '20%'}}/>
</Box>
<Stack sx={{width: 'calc(95% - 100px)', pr: 2, ml: 1}}>
<Box>{row.name}</Box>
<FormControlLabel sx={{mt: 5, pr: 2}}
onClick={() => changeHandler(row, !row.inMenu)}
value="bottom"
control={
<Switch color="primary" checked={row.inMenu}/>
}
label="В меню"
labelPlacement="start"
/>
</Stack>
<Stack direction='row'>
<Stack sx={{width: '5%', mt: 2}} spacing={1} justifyContent='flex-start'>
<IconButton size='small' onClick={() => infoHandler(row)}>
<InfoRoundedIcon/>
</IconButton>
<IconButton size='small' href={`${paths.bar.cocktailEdit}?id=${row.id}`}>
<EditIcon/>
</IconButton>
</Stack>
</Stack>
</Stack>
</Card>
)
}

View File

@@ -0,0 +1,76 @@
import {useMemo, useState} from "react";
import {getComparator} from "../core/getComparator";
import {Accordion, AccordionDetails, AccordionSummary} from "@mui/material";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import Typography from "@mui/material/Typography";
import Box from "@mui/material/Box";
import {CocktailListCard} from "./CocktailListCard";
import {groupByForLoop} from "../core/groupByForLoop";
export function CocktailsList({rows, grouping, changeHandler, infoHandler}) {
const [size, setSize] = useState(20);
window.addEventListener('scroll', () => {
if (window.innerHeight + window.scrollY >= (document.documentElement.scrollHeight - 100)) {
if (!grouping) {
setSize(size + 10)
}
}
});
const visibleRows = useMemo(() => {
let res = [];
if (rows.length === 0) {
return null;
}
if (!grouping) {
return rows
.sort(getComparator("asc", "name"))
.slice(0, size)
.map((row) => {
return (
<CocktailListCard row={row} key={row.id}
changeHandler={changeHandler} infoHandler={infoHandler}/>
)
})
}
const group = groupByForLoop(rows, "category")
if (!group || group.size === 0) {
return null;
}
Array.from(group.keys())
.sort(getComparator())
.map((key) => {
const list = group.get(key);
res.push(
<Accordion key={key} sx={{borderRadius: '5px', mb: 1}}>
<AccordionSummary
expandIcon={<ExpandMoreIcon/>}
aria-controls="panel1-content"
id="panel1-header"
>
<Typography component="span">{key}</Typography>
</AccordionSummary>
<AccordionDetails sx={{p: 1}}>
{list.sort(getComparator("asc", "name"))
.map((row) => {
return (
<CocktailListCard row={row} key={row.id}
changeHandler={changeHandler} infoHandler={infoHandler}/>
)
})}
</AccordionDetails>
</Accordion>
)
})
return res;
// eslint-disable-next-line
}, [size, rows])
return (
<Box mt={2}>
{visibleRows}
</Box>
)
}

View File

@@ -0,0 +1,167 @@
import Box from "@mui/material/Box";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import IconButton from "@mui/material/IconButton";
import AddIcon from "@mui/icons-material/Add";
import * as React from "react";
import {useEffect, useState} from "react";
import {useAlert} from "../../hooks/useAlert";
import {api} from "../../lib/clients/api";
import {requests} from "../../requests";
import {getComparator} from "../core/getComparator";
import {Card} from "@mui/material";
import {SelectEdit} from "./SelectEdit";
import TextField from "@mui/material/TextField";
import DeleteForeverIcon from '@mui/icons-material/DeleteForever';
export function EditCocktailReceipt({receipt, handler}) {
const {createError} = useAlert()
const [ingredients, setIngredients] = useState([]);
const [units, setUnits] = useState([])
useEffect(() => {
api().get(requests.bar.ingredientList)
.then((r) => setIngredients(r.data.sort(getComparator("asc", "name"))))
.catch(() => createError("Ошибка получения списка ингредиентов"))
api().get(requests.bar.unit)
.then((r) => setUnits(r.data.sort(getComparator("asc", "name"))))
.catch(() => createError("Ошибка получения единиц измерения"))
}, []);
const selectHandler = (name, value) => {
const ing = ingredients.find((i) => i.name === value)
const newState = receipt.map((r, i) => {
if (i !== name) {
return r;
}
return {
id: r.id,
ingredient: {
id: ing.id,
isHave: ing.have,
name: ing.name
},
measure: r.measure
}
})
handler("receipt", newState);
checkAllowed(newState);
}
const unitHandler = (name, value) => {
const ing = units.find((i) => i.name === value)
const newState = receipt.map((r, i) => {
if (i !== name) {
return r;
}
return {
id: r.id,
ingredient: r.ingredient,
unit: ing,
count: r.count,
measure: r.measure
}
})
handler("receipt", newState);
checkAllowed(newState);
}
const removeHandler = (index) => {
const arr = receipt.filter((r, i) => i !== index)
handler("receipt", arr)
checkAllowed(arr)
}
const addHandler = () => {
const oldState = receipt;
oldState.push({
id: null,
ingredient: {
id: null,
isHave: false,
name: ""
},
measure: ""
});
handler("receipt", oldState);
checkAllowed(oldState);
}
const checkAllowed = (state) => {
handler("isAllowed", !state.map((r) => r.ingredient.isHave).includes(false))
}
const measureHandler = (index, value) => {
const newState = receipt.map((r, i) => {
if (index !== i) {
return r
}
return {
...r,
measure: value
}
})
handler("receipt", newState)
}
const countHandler = (index, value) => {
const newState = receipt.map((r, i) => {
if (index !== i) {
return r
}
return {
...r,
count: value
}
})
handler("receipt", newState)
}
return (
<Box mb={2}>
{/*Заголовок*/}
<Stack direction='row' justifyContent={'space-between'} sx={{mr: 1}}>
<Typography ml={1} mt={1}>Рецепт</Typography>
<IconButton onClick={() => addHandler()}>
<AddIcon/>
</IconButton>
</Stack>
{/*Рецепт*/}
<Stack sx={{mr: 1}}>
{receipt.map((r, i) => {
return (
<Card key={i} sx={{ml: 0, mb: 1}}>
<Stack>
<Stack direction='row'>
<SelectEdit width={'calc(65% - 28px)'} array={ingredients} value={r.ingredient.name}
handler={selectHandler} label={"Ингредиент"}
margin={1} attributeName={i}
/>
<TextField sx={{width: 'calc(35% - 28px)', mt: 1}}
label={"Кол-во"}
variant="outlined"
disabled
value={r.measure}
onChange={(e) => measureHandler(i, e.target.value)}
/>
<IconButton sx={{mt: 2}}
onClick={() => removeHandler(i)}
>
<DeleteForeverIcon/>
</IconButton>
</Stack>
<Stack direction='row' ml={1} mb={1}>
<TextField sx={{width: 'calc(35% - 28px)', mt: 1}}
label={"Кол-во"}
variant="outlined"
value={r.count}
onChange={(e) => countHandler(i, e.target.value)}
/>
<SelectEdit width={'calc(65% - 28px)'} array={units} value={!r.unit ? null : r.unit.name}
handler={unitHandler} label={"Ед."}
margin={1} attributeName={i}
/>
</Stack>
</Stack>
</Card>
)
})}
</Stack>
</Box>
)
}

View File

@@ -0,0 +1,160 @@
import {Card, FormControl, FormControlLabel, InputAdornment, InputLabel, OutlinedInput} from "@mui/material";
import IconButton from "@mui/material/IconButton";
import SearchIcon from "@mui/icons-material/Search";
import Tooltip from "@mui/material/Tooltip";
import FilterListIcon from "@mui/icons-material/FilterList";
import Box from "@mui/material/Box";
import Grid from "@mui/material/Grid";
import Switch from "@mui/material/Switch";
import CheckMarks from "./CheckMarks";
import Button from "@mui/material/Button";
import * as React from "react";
import {useEffect, useState} from "react";
import {requests} from "../../requests";
import {useAlert} from "../../hooks/useAlert";
import {api} from "../../lib/clients/api";
import {sortList} from "./sortingList";
const inMenuFilter = [
{
id: true,
name: "Есть в меню"
},
{
id: false,
name: "Нет в меню"
}
]
export function FilterBlock({filter, handleFilterChange, handleClearFilter, barmen, all}) {
const {createError} = useAlert();
const [glass, setGlass] = useState([]);
const [category, setCategory] = useState([]);
const [tags, setTags] = useState([])
const alcohol = [
{
name: "Алкогольный",
id: "alcohol1"
},
{
name: "Безалкогольный",
id: "alcohol2"
},
{
name: "Опционально",
id: "alcohol3"
}];
const ingredientCount = [
{
id: "1IngredientCount",
name: 1
},
{
id: "2IngredientCount",
name: 2
},
{
id: "3IngredientCount",
name: 3
},
{
id: "4IngredientCount",
name: 4
},
{
id: "5IngredientCount",
name: 5
}]
useEffect(() => {
api().get(requests.bar.category)
.then((r) => setCategory(r.data))
.catch(() => createError("Ошибка получения категорий"))
api().get(requests.bar.glass)
.then((r) => setGlass(r.data))
.catch(() => createError("Ошибка получения посуды"))
api().get(requests.bar.tags)
.then((r) => setTags(r.data))
.catch(() => createError("Ошибка получения тегов"))
// eslint-disable-next-line
}, []);
return (
<Card>
{/*Строка поиска*/}
<FormControl sx={{m: 1, width: '300px'}}>
<InputLabel htmlFor="outlined-adornment-amount">Поиск</InputLabel>
<OutlinedInput
onChange={(e) => handleFilterChange("search", e.target.value)}
label="With normal TextField"
startAdornment={
<InputAdornment position="start">
<IconButton edge="end">
<SearchIcon/>
</IconButton>
</InputAdornment>
}
/>
</FormControl>
{/*Кнопка открытия фильтров*/}
<Tooltip title="Filter list">
<IconButton onClick={() => handleFilterChange("hidden", !filter.hidden)}>
<FilterListIcon/>
</IconButton>
</Tooltip>
{/*Блок сортировки*/}
<Box hidden={filter.hidden}>
<Grid container>
{/*Фильтр по алкогольности*/}
<CheckMarks rows={sortList} name={"Сортировать по..."} handleChange={handleFilterChange}
filterValue={filter.sorting} filterName={"sorting"} nonMulti/>
</Grid>
</Box>
{/*Блок фильтров*/}
<Box hidden={filter.hidden}>
<Grid container>
{/*Фильтр по меню*/}
{(barmen && all) && (
<CheckMarks rows={inMenuFilter} name={"Есть в меню"} filterName={"inMenu"}
filterValue={filter.inMenu}
handleChange={handleFilterChange}
nonMulti nullValue
/>
)}
{/*Фильтр по избранным*/}
<FormControlLabel
control={
<Switch inputProps={{'aria-label': 'controlled'}}
onChange={() => handleFilterChange("onlyFavourite", !filter.onlyFavourite)}
/>
}
label="Только избранные"
sx={{ml: 1}}
/>
{/*Фильтр по алкогольности*/}
<CheckMarks rows={alcohol} name={"Алкогольность"} handleChange={handleFilterChange}
filterValue={filter.alcohol} filterName={"alcohol"}/>
{/*Фильтр по категории*/}
{category.length > 0 && (
<CheckMarks rows={category} name={"Категории"} filterValue={filter.category}
filterName={"category"} handleChange={handleFilterChange}/>)}
{/*Фильтр по посуде*/}
{glass.length > 0 && (<CheckMarks rows={glass} name={"Подача"} handleChange={handleFilterChange}
filterValue={filter.glass} filterName={"glass"}/>)}
{/*Фильтр по тегам*/}
{tags.length > 0 && (<CheckMarks rows={tags} name={"Теги"} handleChange={handleFilterChange}
filterValue={filter.tags} filterName={"tags"}/>)}
{/*Фильтр по нехватке ингредиентов*/}
{/*todo: доделать эти фильтры в беке*/}
{/*{(barmen && all) && (<CheckMarks rows={ingredientCount} name={"Не хватает ингредиентов"}*/}
{/* handleChange={handleFilterChange}*/}
{/* nonMulti nullValue*/}
{/* filterValue={filter.iCount} filterName={"iCount"}/>)}*/}
<Button onClick={() => handleClearFilter()}>Сбросить</Button>
</Grid>
</Box>
</Card>
)
}

View File

@@ -0,0 +1,17 @@
import Grid from "@mui/material/Grid";
import {Stack} from "@mui/material";
import Typography from "@mui/material/Typography";
import {CocktailItemStyled} from "./CocktailItemStyled";
export function NoResult({load}) {
return (
<Grid item lg={4} md={6} sm={12} xl={3} hidden={!load}>
<CocktailItemStyled>
<Stack align="center" sx={{width: "350px"}}>
<Typography variant="h5" minHeight={'50px'} mt={2}>Нет результатов</Typography>
<Typography>Попробуйте заглянуть позднее</Typography>
</Stack>
</CocktailItemStyled>
</Grid>
)
}

View File

@@ -0,0 +1,25 @@
import {FormControl, InputLabel} from "@mui/material";
import Select from "@mui/material/Select";
import MenuItem from "@mui/material/MenuItem";
import * as React from "react";
export function SelectEdit({label, value, array, handler, attributeName, width, margin}) {
return (
<FormControl sx={{width: width, m: margin}}>
<InputLabel>{label}</InputLabel>
<Select
autoWidth
label={label}
value={!value ? "" : value}
onChange={(e) => handler(attributeName, e.target.value)}
>
<MenuItem value="">
<em>None</em>
</MenuItem>
{array.map((c) => {
return (<MenuItem key={c.id} value={c.name}>{c.name}</MenuItem>)
})}
</Select>
</FormControl>
)
}

View File

@@ -0,0 +1,27 @@
export const sortList = [
{
id: "name|asc",
name: "Название по возрастанию"
},
{
id: "name|desc",
name: "Название по убыванию"
},
// todo: добавить сортировки в беке
// {
// id: "rating.rating|desc",
// name: "Сначала с оценкой"
// },
// {
// id: "rating.rating|asc",
// name: "Сначала без оценки"
// },
// {
// id: "rating.favourite|desc",
// name: "Сначала избранные"
// },
// {
// id: "rating.favourite|asc",
// name: "Сначала не избранные"
// }
]

View File

@@ -0,0 +1,12 @@
import {Backdrop, CircularProgress} from "@mui/material";
export function Loading({loading}) {
return (
<Backdrop
sx={{color: '#fff', zIndex: (theme) => theme.zIndex.drawer + 1}}
open={loading}
>
<CircularProgress color="inherit"/>
</Backdrop>
);
}

View File

@@ -0,0 +1,10 @@
import {AdapterDayjs} from '@mui/x-date-pickers/AdapterDayjs';
import {LocalizationProvider as Provider} from '@mui/x-date-pickers/LocalizationProvider';
import * as React from 'react';
import 'dayjs/locale/ru'
export function LocalizationProvider({children}) {
return (
<Provider dateAdapter={AdapterDayjs} adapterLocale="ru-Ru">{children}</Provider>
);
}

View File

@@ -0,0 +1,38 @@
'use client';
import * as React from 'react';
import Box from '@mui/material/Box';
import { useColorScheme } from '@mui/material/styles';
import {NoSsr} from "./NoSsr";
const HEIGHT = 60;
const WIDTH = 60;
export function Logo({ color = 'dark', emblem, height = HEIGHT, width = WIDTH }) {
let url;
if (emblem) {
url = color === 'light' ? '/assets/logo-emblem.svg' : '/assets/logo-emblem--dark.svg';
} else {
url = color === 'light' ? '/assets/logo.svg' : '/assets/logo--dark.svg';
}
return <Box alt="logo" component="img" height={height} src={url} width={width} />;
}
export function DynamicLogo({
colorDark = 'light',
colorLight = 'dark',
height = HEIGHT,
width = WIDTH,
...props
}) {
const { colorScheme } = useColorScheme();
const color = colorScheme === 'dark' ? colorDark : colorLight;
return (
<NoSsr fallback={<Box sx={{ height: `${height}px`, width: `${width}px` }} />}>
<Logo color={color} height={height} width={width} {...props} />
</NoSsr>
);
}

View File

@@ -0,0 +1,9 @@
import {styled} from "@mui/material/styles";
import Dialog from "@mui/material/Dialog";
export const ModalDialogStyled = styled(Dialog)(({theme}) => ({
backdrop: {
margin: '4px',
border: 'solid',
},
}));

View File

@@ -0,0 +1,25 @@
import * as React from 'react';
import useEnhancedEffect from '@mui/utils/useEnhancedEffect';
export function NoSsr(props) {
const {children, defer = false, fallback = null} = props;
const [mountedState, setMountedState] = React.useState(false);
useEnhancedEffect(() => {
if (!defer) {
setMountedState(true);
}
}, [defer]);
React.useEffect(() => {
if (defer) {
setMountedState(true);
}
}, [defer]);
return (
<>
{mountedState ? children : fallback}
</>
)
}

View File

@@ -0,0 +1,24 @@
import PropTypes from "prop-types";
import * as React from "react";
export function CustomTabPanel(props) {
const {children, value, index, ...other} = props;
return (
<div
role="tabpanel"
hidden={value !== index}
id={`simple-tabpanel-${index}`}
aria-labelledby={`simple-tab-${index}`}
{...other}
>
{value === index && children}
</div>
);
}
CustomTabPanel.propTypes = {
children: PropTypes.node,
index: PropTypes.number.isRequired,
value: PropTypes.number.isRequired,
};

View File

@@ -0,0 +1,81 @@
import {styled, useColorScheme} from "@mui/material/styles";
import Switch from "@mui/material/Switch";
import React from "react";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
export function ThemeSwitch() {
const {mode, setMode} = useColorScheme();
return (
<Box
sx={{
alignItems: 'center',
// backgroundColor: 'var(--mui-palette-neutral-950)',
border: '1px solid var(--mui-palette-neutral-700)',
borderRadius: '12px',
cursor: 'pointer',
display: 'flex',
p: '4px 12px',
}}
>
<StyledSwitch
checked={mode === 'dark'}
onChange={(e) => setMode(e.target.checked ? 'dark' : 'light')}
inputProps={{'aria-label': 'controlled'}}
/>
<Box sx={{flex: '1 1 auto'}}>
<Typography color="inherit" variant="subtitle1">
{(mode === 'dark' ? "Темная " : "Светлая ") + "тема"}
</Typography>
</Box>
</Box>
)
}
const StyledSwitch = styled(Switch)(({theme}) => ({
width: 62,
height: 34,
padding: 7,
'& .MuiSwitch-switchBase': {
margin: 1,
padding: 0,
transform: 'translateX(6px)',
'&.Mui-checked': {
color: '#fff',
transform: 'translateX(22px)',
'& .MuiSwitch-thumb:before': {
backgroundImage: `url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 20 20"><path fill="${encodeURIComponent(
'#fff',
)}" d="M4.2 2.5l-.7 1.8-1.8.7 1.8.7.7 1.8.6-1.8L6.7 5l-1.9-.7-.6-1.8zm15 8.3a6.7 6.7 0 11-6.6-6.6 5.8 5.8 0 006.6 6.6z"/></svg>')`,
},
'& + .MuiSwitch-track': {
opacity: 1,
backgroundColor: theme.palette.mode === 'dark' ? '#8796A5' : '#aab4be',
},
},
},
'& .MuiSwitch-thumb': {
backgroundColor: theme.palette.mode === 'dark' ? '#003892' : '#001e3c',
width: 32,
height: 32,
'&::before': {
content: "''",
position: 'absolute',
width: '100%',
height: '100%',
left: 0,
top: 0,
backgroundRepeat: 'no-repeat',
backgroundPosition: 'center',
backgroundImage: `url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 20 20"><path fill="${encodeURIComponent(
'#fff',
)}" d="M9.305 1.667V3.75h1.389V1.667h-1.39zm-4.707 1.95l-.982.982L5.09 6.072l.982-.982-1.473-1.473zm10.802 0L13.927 5.09l.982.982 1.473-1.473-.982-.982zM10 5.139a4.872 4.872 0 00-4.862 4.86A4.872 4.872 0 0010 14.862 4.872 4.872 0 0014.86 10 4.872 4.872 0 0010 5.139zm0 1.389A3.462 3.462 0 0113.471 10a3.462 3.462 0 01-3.473 3.472A3.462 3.462 0 016.527 10 3.462 3.462 0 0110 6.528zM1.665 9.305v1.39h2.083v-1.39H1.666zm14.583 0v1.39h2.084v-1.39h-2.084zM5.09 13.928L3.616 15.4l.982.982 1.473-1.473-.982-.982zm9.82 0l-.982.982 1.473 1.473.982-.982-1.473-1.473zM9.305 16.25v2.083h1.389V16.25h-1.39z"/></svg>')`,
},
},
'& .MuiSwitch-track': {
opacity: 1,
backgroundColor: theme.palette.mode === 'dark' ? '#8796A5' : '#aab4be',
borderRadius: 20 / 2,
},
}));

View File

@@ -0,0 +1,92 @@
import * as React from 'react';
import Box from '@mui/material/Box';
import Divider from '@mui/material/Divider';
import ListItemIcon from '@mui/material/ListItemIcon';
import MenuItem from '@mui/material/MenuItem';
import MenuList from '@mui/material/MenuList';
import Popover from '@mui/material/Popover';
import Typography from '@mui/material/Typography';
import {SignOut as SignOutIcon} from '@phosphor-icons/react/dist/ssr/SignOut';
import {logger} from "../../lib/DefaultLogger";
import {useAuth} from "../../hooks/useAuth";
import {authClient} from "../../lib/clients/AuthClient";
import {useLocation} from "react-router-dom";
import {useUser} from "../../hooks/useUser";
export function UserPopover({anchorEl, onClose, open}) {
const {checkSession} = useAuth();
const {user, session} = useUser();
const location = useLocation();
const handleSignOut = React.useCallback(async () => {
try {
const {error} = await authClient.signOut();
if (error) {
logger.error('Sign out error', error);
return;
}
// Refresh the auth state
await checkSession?.();
// UserProvider, for this case, will not refresh the router and we need to do it manually
window.location.reload();
// After refresh, AuthGuard will handle the redirect
} catch (err) {
logger.error('Sign out error', err);
}
}, [checkSession, location]);
return (
<Popover
anchorEl={anchorEl}
anchorOrigin={{horizontal: 'left', vertical: 'bottom'}}
onClose={onClose}
open={open}
slotProps={{paper: {sx: {width: '240px'}}}}
>
<Box sx={{p: '16px 20px '}}>
{userDescriptor(user, session)}
</Box>
<Divider/>
<MenuList disablePadding sx={{p: '8px', '& .MuiMenuItem-root': {borderRadius: 1}}}>
{/*<MenuItem component={'a'} href={paths.dashboard.settings} onClick={onClose}>*/}
{/* <ListItemIcon>*/}
{/* <GearSixIcon fontSize="var(--icon-fontSize-md)"/>*/}
{/* </ListItemIcon>*/}
{/* Настройки*/}
{/*</MenuItem>*/}
{/*<MenuItem component={'a'} href={paths.dashboard.account} onClick={onClose}>*/}
{/* <ListItemIcon>*/}
{/* <UserIcon fontSize="var(--icon-fontSize-md)"/>*/}
{/* </ListItemIcon>*/}
{/* Профиль*/}
{/*</MenuItem>*/}
<MenuItem onClick={handleSignOut}>
<ListItemIcon>
<SignOutIcon fontSize="var(--icon-fontSize-md)"/>
</ListItemIcon>
Выход
</MenuItem>
</MenuList>
</Popover>
);
}
function userDescriptor(user, session) {
if (!user) {
return (<Typography variant="subtitle1">Ошибка загрузки данных</Typography>);
}
const open = (session.isActive && user.invited) ? "открыт" : "закрыт";
return (
<>
<Typography variant="subtitle1">{user.name + " " + user.lastName}</Typography>
<Typography color="text.secondary" variant="body2">{user.id}</Typography>
<Typography color="text.secondary" variant="body2">{`Бар ${open}`}</Typography>
</>
);
}

View File

@@ -0,0 +1,29 @@
export function descendingComparator(a, b, orderBy, lastOrder) {
if (getValue(b, orderBy) < getValue(a, orderBy)) {
return -1;
}
if (getValue(b, orderBy) > getValue(a, orderBy)) {
return 1;
}
if (lastOrder && orderBy !== lastOrder) {
if (getValue(b, lastOrder) < getValue(a, lastOrder)) {
return 1;
}
if (getValue(b, lastOrder) > getValue(a, lastOrder)) {
return -1;
}
}
return 0;
}
function getValue(obj, orderBy) {
if (!orderBy) {
return obj;
}
const split = orderBy.split(".")
let res = obj[split[0]];
for (let i = 1; i < split.length; i++) {
res = res[split[i]];
}
return res;
}

View File

@@ -0,0 +1,10 @@
import {descendingComparator} from "./descendingComparator";
export function getComparator(order, orderBy, lastOrder) {
if(!order) {
order = "asc"
}
return order === 'desc'
? (a, b) => descendingComparator(a, b, orderBy, lastOrder)
: (a, b) => -descendingComparator(a, b, orderBy, lastOrder);
}

View File

@@ -0,0 +1,14 @@
export const groupByForLoop = (arr, prop) => {
const grouped = new Map();
for (let i = 0; i < arr.length; i++) {
let key = arr[i][prop];
if (!key) {
key = "Без категории"
}
if (!grouped.has(key)) {
grouped.set(key, []);
}
grouped.get(key).push(arr[i]);
}
return grouped;
};

View File

@@ -0,0 +1,37 @@
import {ChartPie as ChartPieIcon} from '@phosphor-icons/react/dist/ssr/ChartPie';
import {GearSix as GearSixIcon} from '@phosphor-icons/react/dist/ssr/GearSix';
import {PlugsConnected as PlugsConnectedIcon} from '@phosphor-icons/react/dist/ssr/PlugsConnected';
import {User as UserIcon} from '@phosphor-icons/react/dist/ssr/User';
import {Users as UsersIcon} from '@phosphor-icons/react/dist/ssr/Users';
import {XSquare} from '@phosphor-icons/react/dist/ssr/XSquare';
import {
Basket,
BookOpen,
Books,
Cheers,
CoffeeBean,
Coins,
Martini,
Storefront,
Users,
Wallet
} from "@phosphor-icons/react";
export const navIcons = {
'menu': BookOpen,
'list': Books,
'storefront': Storefront,
'wallet': Wallet,
'cocktail': Martini,
'visitors': Users,
'orders': Cheers,
'basket': Basket,
'coins': Coins,
'ingredients': CoffeeBean,
'chart-pie': ChartPieIcon,
'gear-six': GearSixIcon,
'plugs-connected': PlugsConnectedIcon,
'x-square': XSquare,
user: UserIcon,
users: UsersIcon,
}

View File

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

View File

@@ -0,0 +1,65 @@
import * as React from 'react';
import Avatar from '@mui/material/Avatar';
import Box from '@mui/material/Box';
import IconButton from '@mui/material/IconButton';
import Stack from '@mui/material/Stack';
import {List as ListIcon} from '@phosphor-icons/react/dist/ssr/List';
// import NotificationsIcon from '@mui/icons-material/Notifications';
import {usePopover} from "../../hooks/usePopover";
import {MobileNav} from "./MobileNav";
import {UserPopover} from "../core/UserPopover";
// import Tooltip from "@mui/material/Tooltip";
// import {Badge} from "@mui/material";
// import {useAlert} from "../../hooks/useAlert";
export function MainNav() {
const [openNav, setOpenNav] = React.useState(false);
// const {notImplement} = useAlert();
const userPopover = usePopover();
return (
<>
<Box
component="header"
sx={{
borderBottom: '1px solid var(--mui-palette-divider)',
backgroundColor: 'var(--mui-palette-background-paper)',
position: 'sticky',
top: 0,
zIndex: 'var(--mui-zIndex-appBar)',
height: '64px'
}}
>
<Stack direction="row" spacing={3}
sx={{alignItems: 'center', justifyContent: 'space-between', minHeight: '64px', px: 2}}>
<Stack sx={{alignItems: 'center'}} direction="row" spacing={3}>
<IconButton onClick={() => setOpenNav(true)} sx={{display: {xl: 'none'}}}>
<ListIcon/>
</IconButton>
</Stack>
<Stack sx={{alignItems: 'center'}} direction="row" spacing={2}>
{/*<Tooltip title="Уведомления" onClick={() => notImplement()}>*/}
{/* <Badge badgeContent={10} color="success" variant="standart">*/}
{/* <IconButton>*/}
{/* <NotificationsIcon/>*/}
{/* </IconButton>*/}
{/* </Badge>*/}
{/*</Tooltip>*/}
<Avatar onClick={userPopover.handleOpen} ref={userPopover.anchorRef} src="/assets/avatar.png"
sx={{cursor: 'pointer'}}/>
</Stack>
</Stack>
</Box>
<UserPopover anchorEl={userPopover.anchorRef.current} onClose={userPopover.handleClose}
open={userPopover.open}/>
<MobileNav
onClose={() => {
setOpenNav(false);
}}
open={openNav}
/>
</>
);
}

View File

@@ -0,0 +1,38 @@
import * as React from 'react';
import Drawer from '@mui/material/Drawer';
import {NavigationMenu} from "./NavigationMenu";
//Боковое меню
export function MobileNav({open, onClose}) {
return (
<Drawer
PaperProps={{
sx: {
'--MobileNav-background': 'var(--mui-palette-neutral-950)',
'--MobileNav-color': 'var(--mui-palette-common-white)',
'--NavItem-color': 'var(--mui-palette-neutral-300)',
'--NavItem-hover-background': 'rgba(255, 255, 255, 0.04)',
'--NavItem-active-background': 'var(--mui-palette-primary-main)',
'--NavItem-active-color': 'var(--mui-palette-primary-contrastText)',
'--NavItem-disabled-color': 'var(--mui-palette-neutral-500)',
'--NavItem-icon-color': 'var(--mui-palette-neutral-400)',
'--NavItem-icon-active-color': 'var(--mui-palette-primary-contrastText)',
'--NavItem-icon-disabled-color': 'var(--mui-palette-neutral-600)',
bgcolor: 'var(--MobileNav-background)',
color: 'var(--MobileNav-color)',
display: 'flex',
flexDirection: 'column',
maxWidth: '100%',
scrollbarWidth: 'none',
width: 'var(--MobileNav-width)',
zIndex: 'var(--MobileNav-zIndex)',
'&::-webkit-scrollbar': {display: 'none'},
},
}}
onClose={onClose}
open={open}
>
<NavigationMenu/>
</Drawer>
);
}

View File

@@ -0,0 +1,74 @@
import Stack from "@mui/material/Stack";
import {isNavItemActive} from "../../lib/isNavItemActive";
import {navIcons} from "../core/navIcons";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import {Link} from "react-router-dom";
export function renderNavItems({items = [], pathname}) {
const children = items.reduce((acc, curr) => {
const {key, ...item} = curr;
acc.push(<NavItem key={key} pathname={pathname} {...item} />);
return acc;
}, []);
return (
<Stack key={"stack-NavItem-key"} component="ul" spacing={1} sx={{listStyle: 'none', m: 0, p: 0}}>
{children}
</Stack>
);
}
function NavItem({disabled, external, href, icon, matcher, pathname, title}) {
const active = isNavItemActive({disabled, external, href, matcher, pathname});
const Icon = icon ? navIcons[icon] : null;
return (
<li>
<Link to={href} style={{ textDecoration: 'none' }}>
<Box
sx={{
alignItems: 'center',
borderRadius: 1,
color: 'var(--NavItem-color)',
cursor: 'pointer',
display: 'flex',
flex: '0 0 auto',
gap: 1,
p: '6px 16px',
position: 'relative',
textDecoration: 'none',
whiteSpace: 'nowrap',
...(disabled && {
bgcolor: 'var(--NavItem-disabled-background)',
color: 'var(--NavItem-disabled-color)',
cursor: 'not-allowed',
}),
...(active && {
bgcolor: 'var(--NavItem-active-background)',
color: 'var(--NavItem-active-color)'
}),
}}
>
<Box sx={{alignItems: 'center', display: 'flex', justifyContent: 'center', flex: '0 0 auto'}}>
{Icon ? (
<Icon
fill={active ? 'var(--NavItem-icon-active-color)' : 'var(--NavItem-icon-color)'}
fontSize="var(--icon-fontSize-md)"
weight={active ? 'fill' : undefined}
/>
) : null}
</Box>
<Box sx={{flex: '1 1 auto'}}>
<Typography
component="span"
sx={{color: 'inherit', fontSize: '0.875rem', fontWeight: 500, lineHeight: '28px'}}
>
{title}
</Typography>
</Box>
</Box>
</Link>
</li>
);
}

View File

@@ -0,0 +1,56 @@
import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box";
import {ThemeSwitch} from "../core/ThemeSwitch";
import Divider from "@mui/material/Divider";
import {renderNavItems} from "./NavItem";
import {navItems} from "../../navItems";
import React, {useEffect, useState} from "react";
import {useLocation} from "react-router-dom";
import {useUser} from "../../hooks/useUser";
import Typography from "@mui/material/Typography";
function renderSpecialItems(items, label, pathname) {
return (
<Box>
<hr/>
<Typography pl={"20px"} pb={1} variant="subtitle2" color="textSecondary">{label}</Typography>
{renderNavItems({items: items, pathname: pathname})}
</Box>
)
}
export function NavigationMenu() {
const location = useLocation();
const pathname = location.pathname;
const {user} = useUser();
const [items, setItems] = useState(null)
const userChild = navItems.filter((item) => !item.forBarmen && !item.forAdmin)
const barmenChild = navItems.filter((item) => item.forBarmen)
const adminChild = navItems.filter((item) => item.forAdmin)
useEffect(() => {
const role = !user ? "USER" : Object.keys(user).length === 0 ? "USER" : user.role
const newState = (
<Box component="nav" sx={{flex: '1 1 auto', p: '12px'}}>
{renderNavItems({items: userChild, pathname: pathname})}
{role !== "USER" && renderSpecialItems(barmenChild, "Для бармена:", pathname)}
{role === "ADMIN" && renderSpecialItems(adminChild, "Для админа", pathname)}
</Box>
)
setItems(newState)
// eslint-disable-next-line
}, [user, pathname]);
return (
<>
{/*верхняя стопка*/}
<Stack spacing={2} sx={{p: 2, height: '63px'}}>
<ThemeSwitch/>
</Stack>
<Divider sx={{borderColor: 'var(--mui-palette-neutral-700)'}}/>
{/*меню навигации*/}
{items}
</>
)
}

View File

@@ -0,0 +1,37 @@
import * as React from 'react';
import Box from '@mui/material/Box';
import {NavigationMenu} from "./NavigationMenu";
export function SideNav() {
return (
<Box
sx={{
'--SideNav-background': 'var(--mui-palette-neutral-950)',
'--SideNav-color': 'var(--mui-palette-common-white)',
'--NavItem-color': 'var(--mui-palette-neutral-300)',
'--NavItem-hover-background': 'rgba(255, 255, 255, 0.04)',
'--NavItem-active-background': 'var(--mui-palette-primary-main)',
'--NavItem-active-color': 'var(--mui-palette-primary-contrastText)',
'--NavItem-disabled-color': 'var(--mui-palette-neutral-500)',
'--NavItem-icon-color': 'var(--mui-palette-neutral-400)',
'--NavItem-icon-active-color': 'var(--mui-palette-primary-contrastText)',
'--NavItem-icon-disabled-color': 'var(--mui-palette-neutral-600)',
bgcolor: 'var(--SideNav-background)',
color: 'var(--SideNav-color)',
display: {xs: 'none', xl: 'flex'},
flexDirection: 'column',
height: '100%',
left: 0,
maxWidth: '100%',
position: 'fixed',
scrollbarWidth: 'none',
top: 0,
width: 'var(--SideNav-width)',
zIndex: 'var(--SideNav-zIndex)',
'&::-webkit-scrollbar': {display: 'none'},
}}
>
<NavigationMenu/>
</Box>
);
}

View File

@@ -0,0 +1,113 @@
import * as React from "react";
import Box from "@mui/material/Box";
import Paper from "@mui/material/Paper";
import TableContainer from "@mui/material/TableContainer";
import Table from "@mui/material/Table";
import TableBody from "@mui/material/TableBody";
import TableRow from "@mui/material/TableRow";
import TableCell from "@mui/material/TableCell";
import TablePagination from "@mui/material/TablePagination";
import {getComparator} from "../core/getComparator";
import {EnhancedTableToolbar} from "./EnhancedTableToolbar";
import {EnhancedTableHead} from "./EnhancedTableHead";
export default function EnhancedTable({name, rows, cells, handleSelect, filterField, filterEqual, filterValue}) {
//сортировка убывание/возрастание
const [order, setOrder] = React.useState('desc');
//По какому полю сортируем
const [orderBy, setOrderBy] = React.useState('id');
//выбранная страница
const [page, setPage] = React.useState(0);
//количество элементов на странице
const [rowsPerPage, setRowsPerPage] = React.useState(10);
const handleRequestSort = (event, property) => {
const isAsc = orderBy === property && order === 'asc';
setOrder(isAsc ? 'desc' : 'asc');
setOrderBy(property);
};
const handleChangePage = (event, newPage) => {
setPage(newPage);
};
const handleChangeRowsPerPage = (event) => {
setRowsPerPage(parseInt(event.target.value, 10));
setPage(0);
};
const getTableValue = (obj, index) => {
let indexArr = index.split(".");
let object = obj;
for (let i of indexArr) {
object = object[i];
}
return object;
}
const visibleRows = React.useMemo(() =>
[...rows]
.filter((row) => {
if (!filterField) {
return true;
}
for (let field of filterField) {
for (let value of filterValue) {
let eq = (row[field] === value) === filterEqual;
if (!eq) {
return false;
}
}
}
return true;
})
.sort(getComparator(order, orderBy))
.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage),
[order, orderBy, page, rowsPerPage, rows, filterEqual, filterField, filterValue],
);
const renderTable = (row) => {
// const isItemSelected = selected.includes(row.id);
const isItemSelected = false;
return (
<TableRow hover onClick={() => handleSelect(row)} role="checkbox"
aria-checked={isItemSelected} tabIndex={-1} key={row.id} selected={isItemSelected}
sx={{cursor: 'pointer'}}>
{cells.map((cell) => {
return (
<TableCell key={cell.id} sx={{maxWidth: cell.width}}>{getTableValue(row, cell.id)}</TableCell>
)
})}
</TableRow>
);
}
const emptyRow = () => {
return (
<TableRow>
<TableCell colSpan={cells.length}>
Нет заказов
</TableCell>
</TableRow>
)
}
return (
<Box sx={{width: '100%'}}>
<Paper sx={{width: '100%', mb: 2}}>
<EnhancedTableToolbar numSelected={0} name={name}/>
<TableContainer>
<Table sx={{width: 'calc(100% - 30px)'}} aria-labelledby="tableTitle" size="medium">
<EnhancedTableHead numSelected={0} order={order} orderBy={orderBy}
onRequestSort={handleRequestSort}
rowCount={rows.length} cells={cells}/>
<TableBody>
{visibleRows.map((row) => renderTable(row))}
{visibleRows.length === 0 && emptyRow()}
</TableBody>
</Table>
</TableContainer>
<TablePagination rowsPerPageOptions={[5, 10, 25]} component="div" count={visibleRows.length}
rowsPerPage={rowsPerPage}
page={page} onPageChange={handleChangePage}
onRowsPerPageChange={handleChangeRowsPerPage}/>
</Paper>
</Box>
);
}

View File

@@ -0,0 +1,32 @@
import TableHead from "@mui/material/TableHead";
import TableRow from "@mui/material/TableRow";
import TableCell from "@mui/material/TableCell";
import TableSortLabel from "@mui/material/TableSortLabel";
import Box from "@mui/material/Box";
import {visuallyHidden} from "@mui/utils";
import * as React from "react";
export function EnhancedTableHead(props) {
const {order, orderBy, onRequestSort, cells} = props;
const createSortHandler = (property) => (event) => {onRequestSort(event, property);};
return (
<TableHead>
<TableRow>
{cells.map((headCell) => (
<TableCell key={headCell.id} align={"left"} padding={headCell.disablePadding ? 'none' : 'normal'}
sortDirection={orderBy === headCell.id ? order : false} sx={{pl: 1, maxWidth: headCell.width}}>
<TableSortLabel active={orderBy === headCell.id} direction={orderBy === headCell.id ? order : 'asc'} onClick={createSortHandler(headCell.id)}>
{headCell.label}
{orderBy === headCell.id ? (
<Box component="span" sx={visuallyHidden}>
{order === 'desc' ? 'sorted descending' : 'sorted ascending'}
</Box>
) : null}
</TableSortLabel>
</TableCell>
))}
</TableRow>
</TableHead>
);
}

View File

@@ -0,0 +1,64 @@
import Toolbar from "@mui/material/Toolbar";
import {alpha} from "@mui/material/styles";
import Typography from "@mui/material/Typography";
import Tooltip from "@mui/material/Tooltip";
import IconButton from "@mui/material/IconButton";
import DeleteIcon from "@mui/icons-material/Delete";
import FilterListIcon from "@mui/icons-material/FilterList";
import PropTypes from "prop-types";
import * as React from "react";
export function EnhancedTableToolbar(props) {
const { numSelected, name } = props;
return (
<Toolbar
sx={[
{
pl: { sm: 2 },
pr: { xs: 1, sm: 1 },
},
numSelected > 0 && {
bgcolor: (theme) =>
alpha(theme.palette.primary.main, theme.palette.action.activatedOpacity),
},
]}
>
{numSelected > 0 ? (
<Typography
sx={{ flex: '1 1 100%' }}
color="inherit"
variant="subtitle1"
component="div"
>
{numSelected} selected
</Typography>
) : (
<Typography
sx={{ flex: '1 1 100%' }}
variant="h6"
id="tableTitle"
component="div"
>
{name}
</Typography>
)}
{numSelected > 0 ? (
<Tooltip title="Delete">
<IconButton>
<DeleteIcon />
</IconButton>
</Tooltip>
) : (
<Tooltip title="Filter list">
<IconButton>
<FilterListIcon />
</IconButton>
</Tooltip>
)}
</Toolbar>
);
}
EnhancedTableToolbar.propTypes = {
numSelected: PropTypes.number.isRequired,
};

View File

@@ -0,0 +1,95 @@
import * as React from 'react';
import {useEffect, useState} from 'react';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Dialog from '@mui/material/Dialog';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogContentText from '@mui/material/DialogContentText';
import DialogTitle from '@mui/material/DialogTitle';
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import {ButtonGroup} from "@mui/material";
import {requests} from "../../requests";
import {useAlert} from "../../hooks/useAlert";
import {api} from "../../lib/clients/api";
function renderButtons(row, my, handleChange) {
if (my) {
if (row.status === "NEW") {
return (
<ButtonGroup variant="contained">
<Button color="error" onClick={() => handleChange(row, "CANCEL")}>Отмена</Button>
</ButtonGroup>
);
} else {
return null;
}
} else {
return (
<ButtonGroup variant="contained">
<Button color="success" onClick={() => handleChange(row, "DONE")}>Выполнен</Button>
<Button color="error" onClick={() => handleChange(row, "CANCEL")}>Отмена</Button>
</ButtonGroup>
)
}
}
export default function OrderModal({row, handleClose, open, handleChange, my}) {
const [receipt, setReceipt] = useState([]);
const {createError} = useAlert();
useEffect(() => {
if (!row) {
return;
}
api().get(requests.bar.receipts + row.cocktail.id)
.then((r) => setReceipt(r.data))
.catch(() => createError("Ошибка получения рецепта"))
// eslint-disable-next-line
}, [row]);
if (!row) {
return null;
}
return (
<Dialog
fullWidth={true}
maxWidth="350px"
open={open}
onClose={handleClose}
>
<DialogTitle>{"Заказ №" + row.id}</DialogTitle>
<DialogContent>
<DialogContentText>{row.cocktail.name}</DialogContentText>
<DialogContentText>{row.cocktail.alcoholic + " " + row.cocktail.category}</DialogContentText>
<DialogContentText>{"для: " + row.visitor.name + " " + row.visitor.lastName}</DialogContentText>
<Box noValidate component="form"
sx={{display: 'flex', flexDirection: 'column', m: 'auto', width: 'fit-content',}}>
<Stack>
<img src={row.cocktail.image} alt={row.cocktail.name} loading={"eager"} width={"300"}/>
<Typography>Ингредиенты:</Typography>
<Stack pl={1}>
{receipt.map((r) => {
return (<Typography key={r.id}>{`${r.ingredient.name} - ${r.measure}`}</Typography>)
})}
</Stack>
<Typography>Инструкция:</Typography>
<Typography pl={1}>{row.cocktail.instructions}</Typography>
{row.cocktail.video && (<iframe width="350" /*height="315"*/
src={row.cocktail.video}
title="YouTube video player"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerPolicy="strict-origin-when-cross-origin"
allowFullScreen></iframe>)}
</Stack>
</Box>
</DialogContent>
<DialogActions sx={{justifyContent: "space-between"}}>
{renderButtons(row, my, handleChange)}
<Button onClick={handleClose}>Close</Button>
</DialogActions>
</Dialog>
);
}

View File

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

View File

@@ -0,0 +1,49 @@
import Typography from "@mui/material/Typography";
import {Card, FormControlLabel} from "@mui/material";
import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box";
import Switch from "@mui/material/Switch";
import * as React from "react";
export function VisitorItem({visitor, changeHandler, open}) {
const getRole = (role) => {
switch (role) {
case "USER":
return 'Посетитель';
case "BARMEN":
return 'Бармен';
case "ADMIN":
return 'Админ';
default:
return "Посетитель";
}
}
return (
<Card sx={{mb: 1, p: 1, borderRadius: '10px', maxWidth: '600px'}}>
<Stack>
<Typography variant='h6'>{`${visitor.name} ${!visitor.lastName ? "" : visitor.lastName}`}</Typography>
<Box display='flex' justifyContent='flex-end'>
<Typography>{getRole(visitor.role)}</Typography>
</Box>
<Box display='flex' justifyContent='flex-start'>
<FormControlLabel
control={
<Switch
checked={visitor.invited}
disabled={open}
onChange={() => changeHandler(visitor)}
/>}
label="Приглашен" labelPlacement='start'/>
</Box>
<Box display='flex' justifyContent='flex-end'>
<Typography
variant='body2'
color={visitor.isActive ? 'green' : 'red'}
>{visitor.isActive ? "В баре" : "Не вошел в бар"}</Typography>
</Box>
</Stack>
</Card>
)
}

View File

@@ -0,0 +1,49 @@
import * as React from 'react';
import {useCallback, useEffect} from 'react';
import {logger} from "../lib/DefaultLogger";
import {tokenUtil} from "../lib/TokenUtil";
export const AuthContext = React.createContext(undefined);
export function AuthProvider({children}) {
const [state, setState] = React.useState({
auth: false,
error: "",
isLoading: true,
});
const checkSession = useCallback(async () => {
try {
if (!await tokenUtil.checkToken(tokenUtil.getToken())) {
setState((prev) => ({...prev, auth: false, error: '', isLoading: false}));
return;
}
setState((prev) => ({...prev, auth: true, error: "", isLoading: false}));
} catch (err) {
logger.error(err);
setState((prev) => ({...prev, auth: false, error: 'Что-то пошло не так', isLoading: false}));
}
updater().then();
// eslint-disable-next-line
}, []);
useEffect(() => {
checkSession()
.catch((err) => {
logger.error(err);
});
// eslint-disable-next-line
}, []);
const updater = async () => {
await new Promise((resolve) => setTimeout(resolve, 1000 * 60 * 60));
checkSession()
.catch((err) => {
logger.error(err)
})
}
return <AuthContext.Provider value={{...state}}>{children}</AuthContext.Provider>;
}
export const AuthConsumer = AuthContext.Consumer;

View File

@@ -0,0 +1,66 @@
import * as React from "react";
import {logger} from "../lib/DefaultLogger";
import {userClient} from "../lib/clients/UserClient";
import {tokenUtil} from "../lib/TokenUtil";
import {createContext, useCallback, useEffect, useState} from "react";
import {api} from "../lib/clients/api";
import {requests} from "../requests";
export const UserContext = createContext(undefined);
export function UserProvider({children}) {
const refresh = () => {
checkSession()
.catch((err) => logger.error(err))
}
const [state, setState] = useState({
user: {},
session: {},
error: "",
isLoading: true,
refresh: refresh
});
const checkSession = useCallback(async () => {
try {
setState((prev) => ({...prev, isLoading: true}));
if (!await tokenUtil.checkToken(tokenUtil.getToken())) {
setState((prev) => ({...prev, error: '', isLoading: false, user: {}}));
return;
}
api().get(requests.bar.session.status)
.then((r) => setState((prevState) => ({
...prevState,
session: r.data
})))
.catch(() => setState((prevState) => ({
...prevState,
session: {}
})))
if (Object.keys(state.user).length === 0) {
const {data, errorData} = await userClient.getMe();
if (errorData) {
setState((prev) => ({...prev, error: errorData, isLoading: false, user: {}}));
return;
}
setState((prev) => ({...prev, error: "", isLoading: false, user: data}));
}
} catch (err) {
logger.error(err);
setState((prev) => ({...prev, error: 'Что-то пошло не так', isLoading: false, user: {}}));
}
}, [state]);
useEffect(() => {
checkSession()
.catch((err) => {
logger.error(err);
});
// eslint-disable-next-line
}, []);
return <UserContext.Provider value={{...state}}>{children}</UserContext.Provider>;
}
export const UserConsumer = UserContext.Consumer;

View File

@@ -0,0 +1,32 @@
import {useSnackbar} from "notistack";
export function useAlert() {
// variant could be success, error, warning, info, or default
const {enqueueSnackbar} = useSnackbar();
function createAlert(message, variant) {
const options = {
...variant,
anchorOrigin: {vertical: 'top', horizontal: 'right'},
}
enqueueSnackbar(message, options);
}
function notImplement() {
createAlert("Данный функционал пока не реализован", {variant: 'warning'});
}
function createError(message) {
createAlert(message, {variant: "error"});
}
function getError() {
createAlert("Ошибка получения данных", {variant: "error"});
}
function createSuccess(message) {
createAlert(message, {variant: "success"});
}
return {createAlert, notImplement, createError, getError, createSuccess}
}

View File

@@ -0,0 +1,14 @@
import * as React from 'react';
import {AuthContext} from "../context/AuthContext";
export function useAuth() {
const context = React.useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within a AuthProvider');
}
window.auth = context;
return context;
}

View File

@@ -0,0 +1,20 @@
import * as React from 'react';
export function usePopover() {
const anchorRef = React.useRef(null);
const [open, setOpen] = React.useState(false);
const handleOpen = React.useCallback(() => {
setOpen(true);
}, []);
const handleClose = React.useCallback(() => {
setOpen(false);
}, []);
const handleToggle = React.useCallback(() => {
setOpen((prevState) => !prevState);
}, []);
return { anchorRef, handleClose, handleOpen, handleToggle, open };
}

View File

@@ -0,0 +1,47 @@
import * as React from 'react';
// IMPORTANT: To prevent infinite loop, `keys` argument must be memoized with React.useMemo hook.
export function useSelection(keys) {
const [selected, setSelected] = React.useState(new Set());
React.useEffect(() => {
setSelected(new Set());
}, [keys]);
const handleDeselectAll = React.useCallback(() => {
setSelected(new Set());
}, []);
const handleDeselectOne = React.useCallback((key) => {
setSelected((prev) => {
const copy = new Set(prev);
copy.delete(key);
return copy;
});
}, []);
const handleSelectAll = React.useCallback(() => {
setSelected(new Set(keys));
}, [keys]);
const handleSelectOne = React.useCallback((key) => {
setSelected((prev) => {
const copy = new Set(prev);
copy.add(key);
return copy;
});
}, []);
const selectedAny = selected.size > 0;
const selectedAll = selected.size === keys.length;
return {
deselectAll: handleDeselectAll,
deselectOne: handleDeselectOne,
selectAll: handleSelectAll,
selectOne: handleSelectOne,
selected,
selectedAny,
selectedAll,
};
}

View File

@@ -0,0 +1,13 @@
import * as React from "react";
import {UserContext} from "../context/UserContext";
export function useUser() {
const context = React.useContext(UserContext);
if (!context) {
throw new Error('useUser must be used within a UserProvider');
}
window.user = context;
return context;
}

13
front/src/index.css Normal file
View File

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

13
front/src/index.js Normal file
View File

@@ -0,0 +1,13 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './app/App';
import './index.css';
import '@fontsource/roboto/300.css';
import '@fontsource/roboto/400.css';
import '@fontsource/roboto/500.css';
import '@fontsource/roboto/700.css';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<App/>
);

View File

@@ -0,0 +1,4 @@
import {createLogger} from "./Logger";
import {config} from "../Config";
export const logger = createLogger({ level: config.logLevel });

65
front/src/lib/Logger.js Normal file
View File

@@ -0,0 +1,65 @@
/* eslint-disable no-console -- Allow */
// NOTE: A tracking system such as Sentry should replace the console
export const LogLevel = {NONE: 'NONE', ERROR: 'ERROR', WARN: 'WARN', DEBUG: 'DEBUG', ALL: 'ALL'};
const LogLevelNumber = {NONE: 0, ERROR: 1, WARN: 2, DEBUG: 3, ALL: 4};
export class Logger {
prefix;
level;
showLevel;
levelNumber;
constructor({prefix = '', level = LogLevel.ALL, showLevel = true}) {
this.prefix = prefix;
this.level = level;
this.levelNumber = LogLevelNumber[this.level];
this.showLevel = showLevel;
}
debug = (...args) => {
if (this.canWrite(LogLevel.DEBUG)) {
this.write(LogLevel.DEBUG, ...args);
}
};
warn = (...args) => {
if (this.canWrite(LogLevel.WARN)) {
this.write(LogLevel.WARN, ...args);
}
};
error = (...args) => {
if (this.canWrite(LogLevel.ERROR)) {
this.write(LogLevel.ERROR, ...args);
}
};
canWrite(level) {
return this.levelNumber >= LogLevelNumber[level];
}
write(level, ...args) {
let prefix = this.prefix;
if (this.showLevel) {
prefix = `- ${level} ${prefix}`;
}
if (level === LogLevel.ERROR) {
console.error(prefix, ...args);
} else {
console.log(prefix, ...args);
}
}
}
// This can be extended to create context specific logger (Server Action, Router Handler, etc.)
// to add context information (IP, User-Agent, timestamp, etc.)
export function createLogger({prefix, level} = {}) {
return new Logger({prefix, level});
}

View File

@@ -0,0 +1,34 @@
import {decodeToken, isExpired} from "react-jwt";
import {requests} from "../requests";
import axios from "axios";
class TokenUtil {
checkToken(token) {
if (token == null || isExpired(token)) {
return false;
}
this.refreshToken();
return true;
}
getToken() {
return localStorage.getItem("token");
}
refreshToken() {
const decoded = decodeToken(this.getToken());
const currentTime = Date.now() / 1000;
if (decoded.exp - currentTime > 43200) {
return
}
axios.post(requests.auth.refresh, {}, {headers: {'Authorization': this.getToken()}})
.then((r) => {
localStorage.setItem("token", r.data.token)
})
}
}
export const tokenUtil = new TokenUtil();

View File

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

View File

@@ -0,0 +1,17 @@
import {requests} from "../../requests";
import {api} from "./api";
class UserClient {
async getMe() {
try{
let url = requests.users.getMe
const response = await api().get(url);
return {data: response.data}
} catch (e) {
return {errorData: e.data}
}
}
}
export const userClient = new UserClient();

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

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

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

View File

@@ -0,0 +1,18 @@
export const sliceData = (rows, page, elementOnPage) => {
if (!rows || rows.length === 0) {
return [];
}
const maxPage = Math.ceil(rows.length / elementOnPage);
// const start = (page - 1) * elementOnPage;
const start = 0;
console.log(maxPage, start)
let end;
if (page === maxPage) {
end = rows.length;
} else {
end = start + elementOnPage;
}
return rows.slice(start, end);
}

23
front/src/logo.svg Normal file
View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg fill="#ffffff" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 512 512" xml:space="preserve">
<g>
<g>
<rect x="169.69" y="496.792" width="231.799" height="15.208"/>
</g>
</g>
<g>
<g>
<path d="M293.189,297.798L471.835,95.29H245.669C241.764,42.091,197.236,0,143.058,0C86.323,0,40.165,46.158,40.165,102.894
c0,56.735,46.157,102.891,102.893,102.891c15.495,0,30.831-3.527,44.722-10.236l90.201,102.25v168.576h-77.876v15.208h170.958
v-15.208h-77.875V297.798z M230.415,95.288h-69l48.752-48.751C221.462,59.967,228.82,76.812,230.415,95.288z M150.662,15.552
c18.474,1.595,35.331,8.928,48.76,20.222l-48.76,48.76V15.552z M135.454,15.542v68.932L86.701,35.775
C100.131,24.481,116.981,17.135,135.454,15.542z M75.947,46.528l48.812,48.76H55.708C57.302,76.811,64.65,59.958,75.947,46.528z
M55.706,110.496h57.044l5.601,6.35l-42.408,42.408C64.647,145.824,57.3,128.97,55.706,110.496z M135.454,190.244
c-18.475-1.594-35.328-8.94-48.758-20.237l41.735-41.735l7.023,7.961V190.244z M150.662,190.224v-36.751l26.603,30.156
C168.817,187.207,159.823,189.424,150.662,190.224z M201.485,188.096l-21.039-23.848h132.009V149.04H167.031l-33.999-38.541
h112.92h192.188l-34,38.54h-64.81v15.208h51.395L285.586,283.43L201.485,188.096z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

14
front/src/navItems.js Normal file
View File

@@ -0,0 +1,14 @@
import {paths} from "./path";
export const navItems = [
{key: 'menu', title: 'Меню', href: paths.dashboard.overview, icon: 'menu'},
{key: 'cocktailList', title: 'Коктейльная база', href: paths.bar.cocktails, icon: 'list'},
{key: 'myOrders', title: "Мои заказы", href: paths.orders.my, icon: 'wallet'},
{key: 'myBar', title: "Мои бары", href: paths.bar.list, icon: 'storefront'},
{key: 'queue', title: 'Очередь заказов', href: paths.bar.ordersQueue, icon: 'orders', forBarmen: true},
{key: 'ingredients', title: 'Ингредиенты в баре', href: paths.bar.ingredients, icon: 'basket', forBarmen: true},
{key: 'visitors', title: "Посетители", href: paths.visitor.inBar, icon: 'visitors', forBarmen: true},
{key: 'editMenu', title: "Редактировать меню", href: paths.bar.menu, icon: 'menu', forBarmen: true},
{key: 'ingredientEdit', title: 'Ингредиенты', href: paths.bar.ingredientEdit, icon: 'ingredients', forAdmin: true},
{key: 'cocktailEdit', title: 'Коктейли', href: paths.bar.cocktailEdit, icon: 'cocktail', forAdmin: true}
];

24
front/src/path.js Normal file
View File

@@ -0,0 +1,24 @@
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'
},
errors: {notFound: '/errors/not-found'},
notFound: '*',
};

67
front/src/requests.js Normal file
View File

@@ -0,0 +1,67 @@
const host = "api/";
const routes = {
auth: host + "auth/",
users: host + "users/",
operations: host + "operations/",
bar: host + "bar/",
session: host + "bar/session",
ingredient: host + "ingredient",
order: host + "order",
cocktails: host + "cocktail",
visitor: host + "visitors"
}
export const requests = {
auth: {
login: routes.auth + "login",
refresh: routes.auth + "refresh",
singOut: "signOut"
},
cocktails: {
menu: routes.cocktails + "/menu",
simple: routes.cocktails + "/simple",
cocktail: routes.cocktails + "?id=",
modal: routes.cocktails + "/modal?id=",
edit: routes.cocktails,
savePhoto: routes.cocktails + "/photo",
favourite: routes.cocktails + "/favourite?id=",
rating: routes.cocktails + "/rating?id=",
receipts: routes.cocktails + "/receipts?id="
},
visitors: {
all: routes.visitor,
invite: routes.visitor + "/invite?"
},
bar: {
list: routes.bar + "list",
addToMyList: routes.bar + "addToMyList",
enter: routes.bar + "enter?id=",
pay: routes.order + "?",
order: routes.order,
myOrders: routes.order + "/my",
purchases: routes.bar + "purchases",
menu: routes.bar + "menu",
ingredients: routes.ingredient,
ingredientSimple: routes.ingredient + "/simple",
ingredient: routes.ingredient,
ingredientList: routes.ingredient + "/all",
glass: routes.bar + "glass",
category: routes.bar + "category",
receipts: routes.bar + "receipt?id=",
tags: routes.bar + "tags",
type: routes.ingredient + "/type",
session: {
status: routes.session + "/info",
change: routes.session
},
unit: routes.bar + "units"
},
users: {
getMe: routes.bar + "getMe",
},
operations: {
getAll: routes.operations,
create: routes.operations,
}
}

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

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

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

View File

@@ -0,0 +1,3 @@
export const MuiAvatar = {
styleOverrides: { root: { fontSize: '14px', fontWeight: 600, letterSpacing: 0 } },
};

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

View File

@@ -0,0 +1,3 @@
export const MuiCardContent = {
styleOverrides: {root: {padding: '32px 24px', '&:last-child': {paddingBottom: '32px'}}},
};

View File

@@ -0,0 +1,4 @@
export const MuiCardHeader = {
defaultProps: {titleTypographyProps: {variant: 'h6'}, subheaderTypographyProps: {variant: 'body2'}},
styleOverrides: {root: {padding: '32px 24px 16px'}},
};

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

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

View File

@@ -0,0 +1,8 @@
export const MuiLink = {
defaultProps: {underline: 'hover'},
styleOverrides: {
root: {
color: 'var(--mui-palette-text-primary)'
}
}
};

View File

@@ -0,0 +1 @@
export const MuiStack = {defaultProps: {useFlexGap: true}};

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

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

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

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