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

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