Merge branch 'dev' into 'main'

Dev

See merge request frontend/marketplace!28
This commit is contained in:
Nastya 2023-08-04 14:49:05 +00:00
commit 1df8502813
48 changed files with 1425 additions and 1643 deletions

@ -12,7 +12,7 @@
"dependencies": { "dependencies": {
"@emotion/react": "^11.10.5", "@emotion/react": "^11.10.5",
"@emotion/styled": "^11.10.5", "@emotion/styled": "^11.10.5",
"@frontend/kitui": "^1.0.16", "@frontend/kitui": "^1.0.17",
"@mui/icons-material": "^5.10.14", "@mui/icons-material": "^5.10.14",
"@mui/material": "^5.10.14", "@mui/material": "^5.10.14",
"@popperjs/core": "^2.11.8", "@popperjs/core": "^2.11.8",

@ -0,0 +1,4 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.17458 14.8781L6.06831 15.6206H6.06831L6.17458 14.8781ZM13.8254 14.8781L13.7191 14.1357H13.7191L13.8254 14.8781ZM6.08301 6.66667C6.08301 4.50355 7.83656 2.75 9.99967 2.75V1.25C7.00813 1.25 4.58301 3.67512 4.58301 6.66667H6.08301ZM6.08301 8.27792V6.66667H4.58301V8.27792H6.08301ZM4.75 12.3175C4.75 12.0528 4.80329 11.8024 4.89902 11.5751L3.51659 10.9929C3.34466 11.4012 3.25 11.8493 3.25 12.3175H4.75ZM6.28085 14.1357C5.40271 14.01 4.75 13.2427 4.75 12.3175H3.25C3.25 13.9506 4.41397 15.3838 6.06831 15.6206L6.28085 14.1357ZM10 14.4675C8.89615 14.4675 7.46917 14.3058 6.28085 14.1357L6.06831 15.6206C7.27007 15.7926 8.78438 15.9675 10 15.9675V14.4675ZM13.7191 14.1357C12.5308 14.3058 11.1038 14.4675 10 14.4675V15.9675C11.2156 15.9675 12.7299 15.7926 13.9317 15.6206L13.7191 14.1357ZM15.25 12.3175C15.25 13.2427 14.5973 14.01 13.7191 14.1357L13.9317 15.6206C15.586 15.3838 16.75 13.9506 16.75 12.3175H15.25ZM15.1009 11.5749C15.1967 11.8023 15.25 12.0527 15.25 12.3175H16.75C16.75 11.8492 16.6553 11.4011 16.4833 10.9927L15.1009 11.5749ZM13.9163 6.66667V8.27698H15.4163V6.66667H13.9163ZM9.99967 2.75C12.1628 2.75 13.9163 4.50355 13.9163 6.66667H15.4163C15.4163 3.67512 12.9912 1.25 9.99967 1.25V2.75ZM16.4833 10.9927C16.3676 10.718 16.2341 10.4521 16.1085 10.2066C15.9792 9.95372 15.8606 9.72663 15.7536 9.49614C15.5397 9.03537 15.4163 8.64925 15.4163 8.27698H13.9163C13.9163 8.97611 14.1479 9.59971 14.393 10.1277C14.5155 10.3915 14.6523 10.6536 14.773 10.8896C14.8975 11.133 15.0087 11.3559 15.1009 11.5749L16.4833 10.9927ZM4.58301 8.27792C4.58301 8.65008 4.45972 9.0361 4.24596 9.49675C4.13904 9.72718 4.02048 9.95421 3.89121 10.207C3.7657 10.4525 3.63223 10.7183 3.51659 10.9929L4.89902 11.5751C4.99122 11.3561 5.10231 11.1333 5.22675 10.8899C5.34743 10.6539 5.48419 10.3919 5.60661 10.1281C5.85154 9.6003 6.08301 8.97685 6.08301 8.27792H4.58301Z" fill="#9A9AAF" />
<path d="M11.667 17.2173C11.3087 17.7796 10.696 18.1502 10.0003 18.1502C9.30467 18.1502 8.69197 17.7796 8.33366 17.2173" stroke-width="1.5" stroke-linecap="round" stroke="#9A9AAF" />
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

@ -0,0 +1,5 @@
<svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="25" height="25" rx="6" fill="#FFE500" fill-opacity="0.5"/>
<path d="M11.2297 6.95313C11.2297 8.90625 12.1527 14.6387 12.5 14.6387C12.8472 14.6387 13.6643 8.90625 13.6643 6.95313C13.6643 5.65104 13.1944 5 12.5 5C11.8055 5 11.2297 5.65104 11.2297 6.95313Z" fill="#FB5607" stroke="#FB5607" stroke-linejoin="round"/>
<circle cx="12.5" cy="18.1936" r="1.38889" fill="#FB5607"/>
</svg>

After

Width:  |  Height:  |  Size: 489 B

@ -0,0 +1,3 @@
<svg width="22" height="19" viewBox="0 0 22 19" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19.5714 7.29869V2.29869C19.5714 1.50971 18.9318 0.870118 18.1429 0.870118L2.42857 0.870117C1.63959 0.870117 1 1.50971 1 2.29869L1 16.5844C1 17.3734 1.63959 18.013 2.42857 18.013L18.1429 18.013C18.9318 18.013 19.5714 17.3734 19.5714 16.5844V11.5844M20.901 6.5844H13.8571C12.2792 6.5844 11 7.86359 11 9.44154C11 11.0195 12.2792 12.2987 13.8571 12.2987H20.901C20.9557 12.2987 21 12.2544 21 12.1997V6.68341C21 6.62873 20.9557 6.5844 20.901 6.5844Z" stroke="#9A9AAF" stroke-width="1.5"/>
</svg>

After

Width:  |  Height:  |  Size: 596 B

@ -1,28 +0,0 @@
import { Box } from "@mui/material";
import CustomAccordionBasket from "./CustomAccordionBasket";
import { cardShadow } from "@root/utils/themes/shadow";
interface Props {
content: { title: string; data: [string, number][] }[];
}
export default function AccordionWrapperBasket({ content }: Props) {
return (
<Box
sx={{
overflow: "hidden",
borderRadius: "12px",
boxShadow: cardShadow,
}}
>
{content.map((accordionItem, index) => (
<CustomAccordionBasket
key={index}
header={accordionItem.title}
dataSection={accordionItem.data}
totalPrice={3920}
/>
))}
</Box>
);
}

@ -1,46 +0,0 @@
import { Typography, useTheme } from "@mui/material";
import { useNavigate } from "react-router-dom";
interface Props {
text1: string;
text2?: string;
}
export default function ComplexNavText({ text1, text2 }: Props) {
const theme = useTheme();
const navigate = useNavigate();
return (
<Typography component="div" sx={{ display: "flex" }}>
<Typography
component="div"
onClick={() => navigate("/tariffs")}
sx={{
cursor: "pointer",
fontWeight: 400,
fontSize: "12px",
lineHeight: "14px",
color: theme.palette.grey2.main,
}}
>
{text1}
</Typography>
{text2 &&
<Typography
component="span"
sx={{
cursor: "default",
fontWeight: 400,
fontSize: "12px",
lineHeight: "14px",
color: theme.palette.fadePurple.main,
textUnderlinePosition: "under",
textDecorationColor: theme.palette.brightPurple.main,
}}
>
{text2}
</Typography>
}
</Typography>
);
}

@ -1,154 +0,0 @@
import { Box, SvgIcon, Typography, useMediaQuery, useTheme } from "@mui/material";
import { useState } from "react";
import ClearIcon from "@mui/icons-material/Clear";
import ExpandIcon from "./icons/ExpandIcon";
interface Props {
header: string;
totalPrice: number;
dataSection: [string, number][];
}
function TotalSum(mass: [string, number][]): number {
let sum: number = 0;
mass.forEach((element) => {
sum += element[1];
});
return sum;
}
export default function CustomAccordionBasket({ header, totalPrice, dataSection }: Props) {
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const upSm = useMediaQuery(theme.breakpoints.up("sm"));
const [isExpanded, setIsExpanded] = useState<boolean>(false);
let sum: number = TotalSum(dataSection);
return (
<Box
sx={{
backgroundColor: "white",
"&:first-of-type": {
borderTopLeftRadius: "12px",
borderTopRightRadius: "12px",
},
"&:last-of-type": {
borderBottomLeftRadius: "12px",
borderBottomRightRadius: "12px",
},
"&:not(:last-of-type)": {
borderBottom: `1px solid ${theme.palette.grey2.main}`,
},
}}
>
<Box
onClick={() => setIsExpanded((prev) => !prev)}
sx={{
height: "72px",
px: "20px",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
cursor: "pointer",
userSelect: "none",
}}
>
<Typography
sx={{
fontSize: upMd ? "20px" : "16px",
lineHeight: upMd ? undefined : "19px",
fontWeight: 500,
color: theme.palette.text.secondary,
px: 0,
}}
>
{header}
</Typography>
<Box
sx={{
display: "flex",
justifyContent: "flex-end",
height: "100%",
alignItems: "center",
gap: upSm ? "111px" : "17px",
}}
>
<Typography sx={{ color: theme.palette.grey3.main, fontSize: upSm ? "20px" : "16px", fontWeight: 500 }}>
{sum} руб.
</Typography>
<Box
sx={{
borderLeft: upSm ? "1px solid #9A9AAF" : "none",
paddingLeft: upSm ? "24px" : 0,
height: "100%",
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
<ExpandIcon isExpanded={isExpanded} />
</Box>
</Box>
</Box>
{isExpanded &&
dataSection.map((item, index) => {
return (
<Box
key={index}
sx={{
px: "20px",
py: upMd ? "25px" : undefined,
pt: upMd ? undefined : "15px",
pb: upMd ? undefined : "25px",
backgroundColor: "#F1F2F6",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
gap: "15px",
}}
>
<Typography
sx={{
fontSize: upMd ? undefined : "16px",
lineHeight: upMd ? undefined : "19px",
color: theme.palette.grey3.main,
}}
>
{item[0]}
</Typography>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
gap: "10px",
alignItems: "center",
width: upSm ? "195px" : "123px",
marginRight: upSm ? "65px" : 0,
}}
>
<Typography sx={{ color: theme.palette.grey3.main, fontSize: upSm ? "20px" : "16px", fontWeight: 500 }}>
{item[1]} руб.
</Typography>
{upSm ? (
<Typography
sx={{
color: theme.palette.text.secondary,
borderBottom: `1px solid ${theme.palette.text.secondary}`,
width: "max-content",
lineHeight: "19px",
}}
>
Удалить
</Typography>
) : (
<SvgIcon component={ClearIcon}></SvgIcon>
)}
</Box>
</Box>
);
})}
</Box>
);
}

@ -1,46 +0,0 @@
import { Dispatch, SetStateAction, useState } from "react";
import { List, ListItem, Typography, useMediaQuery, useTheme } from "@mui/material";
type Props = {
setType: Dispatch<SetStateAction<"templ" | "squiz" | "reducer">>;
_mocsk_: { name: string; type: "templ" | "squiz" | "reducer" }[];
};
export default function CustomRadioButtons({ setType, _mocsk_ }: Props) {
const theme = useTheme();
const upSm = useMediaQuery(theme.breakpoints.up("sm"));
const [active, setActive] = useState<number>(0);
const activeType = (index: number, type: "templ" | "squiz" | "reducer") => {
setActive(index);
setType(type);
};
return (
<List
sx={{
marginLeft: "-10px",
width: upSm ? "430px" : "auto",
display: "flex",
flexWrap: upSm ? "" : "wrap",
fontWeight: "500",
fontSize: " 16px",
whiteSpace: "nowrap",
}}
>
{_mocsk_.map(({ name, type }, index) =>
active === index ? (
<ListItem key={type} onClick={() => activeType(index, type)} sx={{ color: "#7E2AEA", cursor: "pointer" }}>
<Typography component="span" sx={{ borderBottom: "1px solid #7E2AEA", fontSize: " 16px" }}>
{name}
</Typography>
</ListItem>
) : (
<ListItem key={type} onClick={() => activeType(index, type)} sx={{ cursor: "pointer" }}>
{name}
</ListItem>
)
)}
</List>
);
}

@ -1,3 +1,4 @@
import { useState, useRef, useCallback } from "react";
import { import {
Typography, Typography,
Drawer, Drawer,
@ -6,16 +7,15 @@ import {
Box, Box,
IconButton, IconButton,
SvgIcon, SvgIcon,
Icon,
Badge, Badge,
} from "@mui/material"; } from "@mui/material";
import { IconsCreate } from "@root/lib/IconsCreate";
import ArrowBackIcon from "@mui/icons-material/ArrowBack"; import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import ClearIcon from "@mui/icons-material/Clear"; import ClearIcon from "@mui/icons-material/Clear";
import BasketIcon from "../assets/Icons/BasketIcon.svg"; import { useTickets } from "@frontend/kitui";
import SectionWrapper from "./SectionWrapper"; import SectionWrapper from "./SectionWrapper";
import CustomWrapperDrawer from "./CustomWrapperDrawer"; import CustomWrapperDrawer from "./CustomWrapperDrawer";
import CustomButton from "./CustomButton"; import CustomButton from "./CustomButton";
import { NotificationsModal } from "./NotificationsModal";
import { useNavigate } from "react-router"; import { useNavigate } from "react-router";
import { useCart } from "@root/utils/hooks/useCart"; import { useCart } from "@root/utils/hooks/useCart";
import { currencyFormatter } from "@root/utils/currencyFormatter"; import { currencyFormatter } from "@root/utils/currencyFormatter";
@ -26,8 +26,19 @@ import {
} from "@root/stores/cart"; } from "@root/stores/cart";
import { useCustomTariffsStore } from "@root/stores/customTariffs"; import { useCustomTariffsStore } from "@root/stores/customTariffs";
import { useUserStore } from "@root/stores/user"; import { useUserStore } from "@root/stores/user";
import {
updateTickets,
setTicketCount,
useTicketStore,
} from "@root/stores/tickets";
import { ReactComponent as BellIcon } from "@root/assets/Icons/bell.svg";
import { ReactComponent as CartIcon } from "@root/assets/Icons/cart.svg";
export default function Drawers() { export default function Drawers() {
const [openNotificationsModal, setOpenNotificationsModal] =
useState<boolean>(false);
const bellRef = useRef<HTMLButtonElement | null>(null);
const navigate = useNavigate(); const navigate = useNavigate();
const theme = useTheme(); const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md")); const upMd = useMediaQuery(theme.breakpoints.up("md"));
@ -40,6 +51,20 @@ export default function Drawers() {
(state) => state.summaryPriceAfterDiscountsMap (state) => state.summaryPriceAfterDiscountsMap
); );
const userAccount = useUserStore((state) => state.userAccount); const userAccount = useUserStore((state) => state.userAccount);
const { tickets, ticketCount, apiPage, ticketsPerPage } = useTicketStore(
(state) => state
);
useTickets({
url: "https://hub.pena.digital/heruvym/getTickets",
ticketsPerPage,
ticketApiPage: apiPage,
onNewTickets: useCallback((result) => {
if (result.data) updateTickets(result.data);
setTicketCount(result.count);
}, []),
onError: () => {},
});
const basePrice = Object.values(summaryPriceBeforeDiscountsMap).reduce( const basePrice = Object.values(summaryPriceBeforeDiscountsMap).reduce(
(a, e) => a + e, (a, e) => a + e,
@ -54,13 +79,33 @@ export default function Drawers() {
const totalPriceAfterDiscounts = cart.priceAfterDiscounts + discountedPrice; const totalPriceAfterDiscounts = cart.priceAfterDiscounts + discountedPrice;
return ( return (
<IconButton sx={{ p: 0 }}> <Box sx={{ display: "flex", gap: "20px" }}>
<Typography <IconButton
onClick={openCartDrawer} ref={bellRef}
component="div" aria-label="cart"
onClick={() => setOpenNotificationsModal((isOpened) => !isOpened)}
sx={{ sx={{
position: "absolute", cursor: "pointer",
borderRadius: "6px",
background: openNotificationsModal
? theme.palette.brightPurple.main
: theme.palette.background.default,
"& .MuiBadge-badge": {
background: openNotificationsModal
? theme.palette.background.default
: theme.palette.brightPurple.main,
color: openNotificationsModal
? theme.palette.brightPurple.main
: theme.palette.background.default,
},
"& svg > path:first-child": {
fill: openNotificationsModal ? "#FFFFFF" : "#9A9AAF",
},
"& svg > path:last-child": {
stroke: openNotificationsModal ? "#FFFFFF" : "#9A9AAF",
},
"&:hover": { "&:hover": {
background: theme.palette.brightPurple.main,
"& .MuiBox-root": { "& .MuiBox-root": {
background: theme.palette.brightPurple.main, background: theme.palette.brightPurple.main,
}, },
@ -68,6 +113,57 @@ export default function Drawers() {
background: theme.palette.background.default, background: theme.palette.background.default,
color: theme.palette.brightPurple.main, color: theme.palette.brightPurple.main,
}, },
"& svg > path:first-child": { fill: "#FFFFFF" },
"& svg > path:last-child": { stroke: "#FFFFFF" },
},
}}
>
<Badge
badgeContent={ticketCount}
sx={{
"& .MuiBadge-badge": {
display: ticketCount ? "flex" : "none",
color: "#FFFFFF",
background: theme.palette.brightPurple.main,
transform: "scale(0.8) translate(50%, -50%)",
top: "2px",
right: "2px",
fontWeight: 400,
},
}}
>
<BellIcon />
</Badge>
</IconButton>
<NotificationsModal
open={openNotificationsModal}
setOpen={setOpenNotificationsModal}
anchorElement={bellRef.current}
notifications={tickets.map((ticket) => ({
text: "У вас новое сообщение от техподдержки",
date: new Date(ticket.updated_at).toLocaleDateString(),
watched: ticket.user === ticket.top_message.user_id,
}))}
/>
<IconButton
onClick={openCartDrawer}
component="div"
sx={{
cursor: "pointer",
background: theme.palette.background.default,
borderRadius: "6px",
"&:hover": {
background: theme.palette.brightPurple.main,
"& .MuiBox-root": {
background: theme.palette.brightPurple.main,
},
"& .MuiBadge-badge": {
background: theme.palette.background.default,
color: theme.palette.brightPurple.main,
},
"& svg > path:nth-child(1)": { fill: "#FFFFFF" },
"& svg > path:nth-child(2)": { fill: "#FFFFFF" },
"& svg > path:nth-child(3)": { stroke: "#FFFFFF" },
}, },
}} }}
> >
@ -75,49 +171,19 @@ export default function Drawers() {
badgeContent={userAccount?.cart.length} badgeContent={userAccount?.cart.length}
sx={{ sx={{
"& .MuiBadge-badge": { "& .MuiBadge-badge": {
display: userAccount?.cart.length ? "flex" : "none",
color: "#FFFFFF", color: "#FFFFFF",
background: theme.palette.brightPurple.main, background: theme.palette.brightPurple.main,
transform: "scale(0.8) translate(50%, -50%)", transform: "scale(0.8) translate(50%, -50%)",
top: "10px", top: "2px",
right: "10px", right: "2px",
fontWeight: 400, fontWeight: 400,
}, },
}} }}
> >
<IconsCreate svg={BasketIcon} bgcolor="#F2F3F7" /> <CartIcon />
</Badge> </Badge>
</Typography> </IconButton>
{cart.itemCount && (
<Icon
component="div"
sx={{
position: "relative",
left: "8px",
bottom: "7px",
width: "16px",
height: "16px",
backgroundColor: "#7E2AEA",
borderRadius: "12px",
}}
>
<Typography
component="div"
sx={{
display: "flex",
fontSize: "12px",
mt: "4.5px",
width: "100%",
height: "9px",
color: "white",
alignItems: "center",
justifyContent: "center",
}}
>
{cart.itemCount}
</Typography>
</Icon>
)}
<Drawer anchor={"right"} open={isDrawerOpen} onClose={closeCartDrawer}> <Drawer anchor={"right"} open={isDrawerOpen} onClose={closeCartDrawer}>
<SectionWrapper <SectionWrapper
maxWidth="lg" maxWidth="lg"
@ -247,6 +313,6 @@ export default function Drawers() {
</Box> </Box>
</SectionWrapper> </SectionWrapper>
</Drawer> </Drawer>
</IconButton> </Box>
); );
} }

@ -1,95 +1,100 @@
import { import {
FormControl, FormControl,
InputLabel, InputLabel,
SxProps, SxProps,
TextField, TextField,
TextFieldProps, TextFieldProps,
Theme, Theme,
useMediaQuery, useMediaQuery,
useTheme, useTheme,
} from "@mui/material"; } from "@mui/material";
import "./text-input.css"; import "./text-input.css";
interface Props { interface Props {
id: string; id: string;
label?: string; label?: string;
bold?: boolean; bold?: boolean;
gap?: string; gap?: string;
color?: string; color?: string;
FormInputSx?: SxProps<Theme>; FormInputSx?: SxProps<Theme>;
TextfieldProps: TextFieldProps; TextfieldProps: TextFieldProps;
onChange: (e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => void; onChange: (
e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>
) => void;
} }
export default function InputTextfield({ export default function InputTextfield({
id, id,
label, label,
bold = false, bold = false,
gap = "10px", gap = "10px",
onChange, onChange,
TextfieldProps, TextfieldProps,
color, color,
FormInputSx, FormInputSx,
}: Props) { }: Props) {
const theme = useTheme(); const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md")); const upMd = useMediaQuery(theme.breakpoints.up("md"));
const labelFont = upMd const labelFont = upMd
? bold ? bold
? theme.typography.p1 ? theme.typography.p1
: { ...theme.typography.body1, fontWeight: 500 } : { ...theme.typography.body1, fontWeight: 500 }
: theme.typography.body2; : theme.typography.body2;
const placeholderFont = upMd ? undefined : { fontWeight: 400, fontSize: "16px", lineHeight: "19px" }; const placeholderFont = upMd
? undefined
: { fontWeight: 400, fontSize: "16px", lineHeight: "19px" };
return ( return (
<FormControl <FormControl
fullWidth fullWidth
variant="standard" variant="standard"
sx={{ sx={{
gap, gap,
// mt: "10px", // mt: "10px",
...FormInputSx, ...FormInputSx,
}} }}
>
{label && (
<InputLabel
shrink
htmlFor={id}
sx={{
position: "inherit",
color: "black",
transform: "none",
...labelFont,
}}
> >
{label && {label}
<InputLabel </InputLabel>
shrink )}
htmlFor={id} <TextField
sx={{ {...TextfieldProps}
position: "inherit", fullWidth
color: "black", id={id}
transform: "none", sx={{
...labelFont, "& .MuiInputBase-root": {
}} height: "48px",
> borderRadius: "8px",
{label} },
</InputLabel> }}
} inputProps={{
<TextField sx: {
{...TextfieldProps} boxSizing: "border-box",
fullWidth backgroundColor: color,
id={id} border: "1px solid" + theme.palette.grey2.main,
sx={{ borderRadius: "8px",
"& .MuiInputBase-root": { height: "48px",
height: "48px", py: 0,
borderRadius: "8px", color: "black",
}, ...placeholderFont,
}} },
inputProps={{ }}
sx: { onChange={onChange}
backgroundColor: color, />
border: "1px solid" + theme.palette.grey2.main, </FormControl>
borderRadius: "8px", );
height: "48px",
py: 0,
color: "black",
...placeholderFont,
},
}}
onChange={onChange}
/>
</FormControl>
);
} }

@ -1,13 +1,10 @@
import { Outlet } from "react-router-dom"; import { Outlet } from "react-router-dom";
import Navbar from "./Navbar/Navbar"; import Navbar from "./Navbar/Navbar";
export default function Layout() { export default function Layout() {
return (
return ( <Navbar isLoggedIn={true}>
<> <Outlet />
<Navbar isLoggedIn={true} /> </Navbar>
<Outlet /> );
</> }
);
}

@ -1,29 +1,20 @@
import { useState } from "react"; import { useState } from "react";
import { TransitionProps } from "@mui/material/transitions"; import { Link, useLocation } from "react-router-dom";
import logotip from "../../assets/Icons/logoPenaHab.svg";
import logotipBlack from "../../assets/Icons/black_logo_PenaHab.svg";
import CustomAvatar from "./Avatar";
import CloseIcon from "../icons/CloseIcons";
import React from "react";
import { import {
AppBar,
Box, Box,
Button, Button,
Dialog,
IconButton,
List, List,
ListItem, ListItem,
Slide,
Toolbar,
Typography, Typography,
useMediaQuery, useMediaQuery,
useTheme, useTheme,
} from "@mui/material"; } from "@mui/material";
import { Link, useLocation } from "react-router-dom";
import { useUserStore } from "@root/stores/user"; import { useUserStore } from "@root/stores/user";
import { currencyFormatter } from "@root/utils/currencyFormatter"; import { currencyFormatter } from "@root/utils/currencyFormatter";
import CustomAvatar from "./Avatar";
type MenuItem = { type MenuItem = {
name: string; name: string;
url: string; url: string;
@ -46,22 +37,11 @@ const arrayMenu: MenuItem[] = [
{ name: "История", url: "/history" }, { name: "История", url: "/history" },
]; ];
const Transition = React.forwardRef(function Transition(
props: TransitionProps & {
children: React.ReactElement;
},
ref: React.Ref<null>
) {
return <Slide direction="right" ref={ref} {...props} />;
});
interface DialogMenuProps { interface DialogMenuProps {
open: boolean;
handleClose: () => void; handleClose: () => void;
} }
export default function DialogMenu({ open, handleClose }: DialogMenuProps) { export default function DialogMenu({ handleClose }: DialogMenuProps) {
const [activeSubMenuIndex, setActiveSubMenuIndex] = useState<number>(-1); const [activeSubMenuIndex, setActiveSubMenuIndex] = useState<number>(-1);
const theme = useTheme(); const theme = useTheme();
const location = useLocation(); const location = useLocation();
@ -81,64 +61,12 @@ export default function DialogMenu({ open, handleClose }: DialogMenuProps) {
); );
return ( return (
<Dialog <Box sx={{ height: "100%", maxHeight: "calc(100vh - 51px)" }}>
fullScreen
sx={{
zIndex: 1501,
width: isMobile ? "100%" : "320px",
mr: "auto",
height: "100%",
"& .MuiPaper-root.MuiDialog-paper": {
background: theme.palette.background.default,
},
}}
open={open}
onClose={closeDialogMenu}
TransitionComponent={Transition}
>
<AppBar
sx={{
position: "relative",
display: "flex",
justifyContent: "center",
columnGap: "100px",
background: location.pathname === "/" ? "#333647" : "#FFFFFF",
boxShadow: "none",
height: isMobile ? "66px" : "100px",
}}
>
<Toolbar
sx={{
position: "relative",
display: "flex",
justifyContent: "flex-start",
svg: { color: "#000000" },
}}
>
<IconButton
edge="start"
color="inherit"
onClick={closeDialogMenu}
aria-label="close"
>
<CloseIcon />
</IconButton>
{isMobile && (
<Box sx={{ ml: "auto" }}>
<img
src={location.pathname === "/" ? logotip : logotipBlack}
alt="icon"
/>
</Box>
)}
</Toolbar>
</AppBar>
<List <List
sx={{ sx={{
maxWidth: "250px", maxWidth: "250px",
background: location.pathname === "/" ? "#333647" : "#FFFFFF", background: location.pathname === "/" ? "#333647" : "#FFFFFF",
height: "100vh", height: "100%",
p: "0",
}} }}
> >
<ListItem <ListItem
@ -229,76 +157,61 @@ export default function DialogMenu({ open, handleClose }: DialogMenuProps) {
</Box> </Box>
))} ))}
</ListItem> </ListItem>
{isMobile ? ( {location.pathname === "/" ? (
location.pathname === "/" ? ( <Button
<Button component={Link}
component={Link} to={user ? "/tariffs" : "/signin"}
to={user ? "/tariffs" : "/signin"} state={user ? undefined : { backgroundLocation: location }}
state={user ? undefined : { backgroundLocation: location }} variant="outlined"
variant="outlined" sx={{
sx={{ width: "188px",
width: "188px", color: "white",
color: "white", border: "1px solid white",
border: "1px solid white", ml: "40px",
ml: "40px", mt: "35px",
mt: "35px", textTransform: "none",
textTransform: "none", fontWeight: "400",
fontWeight: "400", fontSize: "18px",
fontSize: "18px", lineHeight: "24px",
lineHeight: "24px", borderRadius: "8px",
borderRadius: "8px", padding: "8px 17px",
padding: "8px 17px", }}
}} >
> Личный кабинет
Личный кабинет </Button>
</Button>
) : (
<Box
sx={{
width: "100%",
height: "72px",
background: "#F2F3F7",
display: "flex",
alignItems: "center",
position: "absolute",
bottom: "0",
}}
>
<CustomAvatar />
<Box sx={{ ml: "8px", whiteSpace: "nowrap" }}>
<Typography
sx={{
fontSize: "12px",
lineHeight: "14px",
color: theme.palette.grey3.main,
}}
>
Мой баланс
</Typography>
<Typography
variant="body2"
color={theme.palette.brightPurple.main}
>
{currencyFormatter.format(cash / 100)}
</Typography>
</Box>
</Box>
)
) : ( ) : (
<Box <Box
sx={{ sx={{
width: "100%",
height: "72px",
background: "#F2F3F7",
display: "flex",
alignItems: "center",
position: "absolute", position: "absolute",
right: "40px", bottom: "0",
bottom: "60px",
}} }}
> >
<img <CustomAvatar />
src={location.pathname === "/" ? logotip : logotipBlack} <Box sx={{ ml: "8px", whiteSpace: "nowrap" }}>
alt="icon" <Typography
/> sx={{
fontSize: "12px",
lineHeight: "14px",
color: theme.palette.grey3.main,
}}
>
Мой баланс
</Typography>
<Typography
variant="body2"
color={theme.palette.brightPurple.main}
>
{currencyFormatter.format(cash / 100)}
</Typography>
</Box>
</Box> </Box>
)} )}
</List> </List>
</Dialog> </Box>
); );
} }

@ -2,14 +2,24 @@ import { useMediaQuery, useTheme } from "@mui/material";
import NavbarCollapsed from "./NavbarCollapsed"; import NavbarCollapsed from "./NavbarCollapsed";
import NavbarFull from "./NavbarFull"; import NavbarFull from "./NavbarFull";
import type { ReactNode } from "react";
interface Props { interface Props {
isCollapsed?: boolean;
isLoggedIn: boolean; isLoggedIn: boolean;
children: ReactNode;
} }
export default function Navbar({ isLoggedIn, isCollapsed = false }: Props) { export default function Navbar({ isLoggedIn, children }: Props) {
const theme = useTheme(); const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md")); const upMd = useMediaQuery(theme.breakpoints.up("md"));
return upMd ? <NavbarFull isLoggedIn={isLoggedIn} /> : <NavbarCollapsed isLoggedIn={isLoggedIn} />; return (
<>
{upMd ? (
<NavbarFull isLoggedIn={isLoggedIn}>{children}</NavbarFull>
) : (
<NavbarCollapsed isLoggedIn={isLoggedIn}>{children}</NavbarCollapsed>
)}
</>
);
} }

@ -1,34 +1,70 @@
import { useState } from "react"; import { useState, useRef, useEffect, useCallback } from "react";
import { Badge, IconButton, useTheme } from "@mui/material"; import { Box, Badge, Drawer, IconButton, useTheme } from "@mui/material";
import MenuIcon from "@mui/icons-material/Menu";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { useTickets } from "@frontend/kitui";
import SectionWrapper from "../SectionWrapper"; import SectionWrapper from "../SectionWrapper";
import { NotificationsModal } from "../NotificationsModal";
import { useUserStore } from "@root/stores/user"; import { useUserStore } from "@root/stores/user";
import {
updateTickets,
setTicketCount,
useTicketStore,
} from "@root/stores/tickets";
import MenuIcon from "@mui/icons-material/Menu";
import { ReactComponent as CartIcon } from "@root/assets/Icons/cart.svg";
import { ReactComponent as BellIcon } from "@root/assets/Icons/bell.svg";
import PenaLogo from "../PenaLogo";
import DialogMenu from "./DialogMenu"; import DialogMenu from "./DialogMenu";
import PenaLogo from "../PenaLogo";
import CloseIcon from "../icons/CloseIcons";
import cartIcon from "@root/assets/Icons/cart.svg"; import type { ReactNode } from "react";
interface Props { interface Props {
isLoggedIn: boolean; isLoggedIn: boolean;
children: ReactNode;
} }
export default function NavbarCollapsed({ isLoggedIn }: Props) { export default function NavbarCollapsed({ isLoggedIn, children }: Props) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [openNotificationsModal, setOpenNotificationsModal] =
useState<boolean>(false);
const bellRef = useRef<HTMLButtonElement | null>(null);
const userAccount = useUserStore((state) => state.userAccount); const userAccount = useUserStore((state) => state.userAccount);
const { ticketCount, tickets, apiPage, ticketsPerPage } = useTicketStore(
(state) => state
);
useTickets({
url: "https://hub.pena.digital/heruvym/getTickets",
ticketsPerPage,
ticketApiPage: apiPage,
onNewTickets: useCallback((result) => {
if (result.data) updateTickets(result.data);
setTicketCount(result.count);
}, []),
onError: () => {},
});
const theme = useTheme(); const theme = useTheme();
const handleClickOpen = () => {
setOpen(true);
};
const handleClose = () => { const handleClose = () => {
setOpen(false); setOpen(false);
}; };
useEffect(() => {
if (open) {
document.body.style.overflow = "hidden";
return;
}
document.body.style.overflow = "unset";
}, [open]);
return ( return (
<SectionWrapper <SectionWrapper
component="nav" component="nav"
@ -37,28 +73,77 @@ export default function NavbarCollapsed({ isLoggedIn }: Props) {
backgroundColor: theme.palette.navbarbg.main, backgroundColor: theme.palette.navbarbg.main,
position: "sticky", position: "sticky",
top: 0, top: 0,
zIndex: 1501,
// borderBottom: "1px solid #E3E3E3",
}}
sx={{
height: "51px",
py: "6px",
display: "flex",
justifyContent: "flex-start",
alignItems: "center",
columnGap: "10px",
}} }}
sx={{ height: "51px", padding: "0" }}
> >
<IconButton <Box
onClick={handleClickOpen} sx={{
sx={{ p: 0, width: "30px", color: theme.palette.primary.main }} display: "flex",
columnGap: "10px",
alignItems: "center",
height: "100%",
padding: "0 18px",
}}
> >
<MenuIcon sx={{ height: "30px", width: "30px" }} />
</IconButton>
<Link to="/cart">
<IconButton <IconButton
onClick={() => setOpen((isOpened) => !isOpened)}
sx={{
p: 0,
width: "30px",
color: theme.palette.primary.main,
}}
>
{open ? (
<CloseIcon />
) : (
<MenuIcon sx={{ height: "30px", width: "30px" }} />
)}
</IconButton>
<Link to="/cart">
<IconButton
aria-label="cart"
sx={{
width: "30px",
height: "30px",
background: theme.palette.background.default,
borderRadius: "6px",
"&:hover": {
background: theme.palette.brightPurple.main,
"& .MuiBadge-badge": {
background: theme.palette.background.default,
color: theme.palette.brightPurple.main,
},
"& svg > path:nth-child(1)": { fill: "#FFFFFF" },
"& svg > path:nth-child(2)": { fill: "#FFFFFF" },
"& svg > path:nth-child(3)": { stroke: "#FFFFFF" },
},
}}
>
<Badge
badgeContent={userAccount?.cart.length}
sx={{
"& .MuiBadge-badge": {
display: userAccount?.cart.length ? "flex" : "none",
color: "#FFFFFF",
background: theme.palette.brightPurple.main,
transform: "scale(0.7) translate(50%, -50%)",
top: "2px",
right: "3px",
fontWeight: 400,
},
}}
>
<CartIcon />
</Badge>
</IconButton>
</Link>
<IconButton
ref={bellRef}
onClick={() => setOpenNotificationsModal((isOpened) => !isOpened)}
aria-label="cart" aria-label="cart"
sx={{ sx={{
width: "30px",
height: "30px",
background: theme.palette.background.default, background: theme.palette.background.default,
borderRadius: "6px", borderRadius: "6px",
"&:hover": { "&:hover": {
@ -67,30 +152,84 @@ export default function NavbarCollapsed({ isLoggedIn }: Props) {
background: theme.palette.background.default, background: theme.palette.background.default,
color: theme.palette.brightPurple.main, color: theme.palette.brightPurple.main,
}, },
"& svg > path:first-child": { fill: "#FFFFFF" },
"& svg > path:last-child": { stroke: "#FFFFFF" },
}, },
}} }}
> >
<Badge <Badge
badgeContent={userAccount?.cart.length} badgeContent={ticketCount}
sx={{ sx={{
"& .MuiBadge-badge": { "& .MuiBadge-badge": {
display: ticketCount ? "flex" : "none",
color: "#FFFFFF", color: "#FFFFFF",
background: theme.palette.brightPurple.main, background: theme.palette.brightPurple.main,
transform: "scale(0.8) translate(50%, -50%)", transform: "scale(0.7) translate(50%, -50%)",
top: "2px", top: "3px",
right: "2px", right: "3px",
fontWeight: 400, fontWeight: 400,
}, },
}} }}
> >
<img src={cartIcon} alt="cart" /> <BellIcon />
</Badge> </Badge>
</IconButton> </IconButton>
</Link> <NotificationsModal
<Link to="/" style={{ marginLeft: "auto" }}> open={openNotificationsModal}
<PenaLogo width={100} /> setOpen={setOpenNotificationsModal}
</Link> anchorElement={bellRef.current}
<DialogMenu open={open} handleClose={handleClose} /> notifications={tickets.map((ticket) => ({
text: "У вас новое сообщение от техподдержки",
date: new Date(ticket.updated_at).toLocaleDateString(),
watched: ticket.user === ticket.top_message.user_id,
}))}
/>
<Link to="/" style={{ marginLeft: "auto" }}>
<PenaLogo width={100} />
</Link>
</Box>
<Box sx={{ display: "flex", overflow: open ? "hidden" : "unset" }}>
<Drawer
sx={{
width: 210,
position: "relative",
zIndex: open ? "none" : "-1",
"& .MuiDrawer-paper": {
position: "absolute",
top: "0",
width: 210,
height: "100%",
},
}}
variant="persistent"
anchor="left"
open={open}
>
<DialogMenu handleClose={handleClose} />
</Drawer>
<Box
sx={{
width: "100%",
minWidth: "100%",
minHeight: "calc(100vh - 51px)",
flexGrow: 1,
transition: theme.transitions.create("margin", {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
marginLeft: `-${210}px`,
...(open && {
transition: theme.transitions.create("margin", {
easing: theme.transitions.easing.easeOut,
duration: theme.transitions.duration.enteringScreen,
}),
marginLeft: 0,
}),
}}
>
{children}
</Box>
</Box>
</SectionWrapper> </SectionWrapper>
); );
} }

@ -9,7 +9,6 @@ import {
} from "@mui/material"; } from "@mui/material";
import SectionWrapper from "../SectionWrapper"; import SectionWrapper from "../SectionWrapper";
import LogoutIcon from "../icons/LogoutIcon"; import LogoutIcon from "../icons/LogoutIcon";
import WalletIcon from "../icons/WalletIcon";
import CustomAvatar from "./Avatar"; import CustomAvatar from "./Avatar";
import Drawers from "../Drawers"; import Drawers from "../Drawers";
import PenaLogo from "../PenaLogo"; import PenaLogo from "../PenaLogo";
@ -19,13 +18,19 @@ import { enqueueSnackbar } from "notistack";
import { clearUserData, useUserStore } from "@root/stores/user"; import { clearUserData, useUserStore } from "@root/stores/user";
import { clearAuthToken, getMessageFromFetchError } from "@frontend/kitui"; import { clearAuthToken, getMessageFromFetchError } from "@frontend/kitui";
import { clearCustomTariffs } from "@root/stores/customTariffs"; import { clearCustomTariffs } from "@root/stores/customTariffs";
import { currencyFormatter } from "@root/utils/currencyFormatter"; import { currencyFormatter } from "@root/utils/currencyFormatter";
import walletIcon from "@root/assets/Icons/wallet_icon.svg";
import type { ReactNode } from "react";
interface Props { interface Props {
isLoggedIn: boolean; isLoggedIn: boolean;
children: ReactNode;
} }
export default function NavbarFull({ isLoggedIn }: Props) { export default function NavbarFull({ isLoggedIn, children }: Props) {
const theme = useTheme(); const theme = useTheme();
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
@ -45,104 +50,115 @@ export default function NavbarFull({ isLoggedIn }: Props) {
} }
} }
return isLoggedIn ? ( return (
<Container <Box>
component="nav" {isLoggedIn ? (
disableGutters <Container
maxWidth={false} component="nav"
sx={{ disableGutters
px: "16px", maxWidth={false}
display: "flex", sx={{
height: "80px", px: "16px",
alignItems: "center", display: "flex",
gap: "60px", height: "80px",
bgcolor: "white", alignItems: "center",
borderBottom: "1px solid #E3E3E3", gap: "60px",
}} bgcolor: "white",
> borderBottom: "1px solid #E3E3E3",
<Link to="/"> }}
<PenaLogo width={124} />
</Link>
<Menu />
<Box
sx={{
display: "flex",
ml: "auto",
}}
>
<Drawers />
<IconButton
sx={{ p: 0, ml: "8px" }}
onClick={() => navigate("/wallet")}
> >
<WalletIcon color={theme.palette.grey2.main} bgcolor="#F2F3F7" /> <Link to="/">
</IconButton> <PenaLogo width={124} />
<Box sx={{ ml: "8px", whiteSpace: "nowrap" }}> </Link>
<Typography <Menu />
<Box sx={{ display: "flex", ml: "auto" }}>
<Drawers />
<IconButton
sx={{
display: "flex",
alignItems: "center",
ml: "20px",
bgcolor: "#F2F3F7",
borderRadius: "6px",
height: "36px",
width: "36px",
}}
onClick={() => navigate("/wallet")}
>
<img src={walletIcon} alt="wallet" />
</IconButton>
<Box sx={{ ml: "8px", whiteSpace: "nowrap" }}>
<Typography
sx={{
fontSize: "12px",
lineHeight: "14px",
color: theme.palette.grey3.main,
}}
>
Мой баланс
</Typography>
<Typography
variant="body2"
color={theme.palette.brightPurple.main}
>
{currencyFormatter.format(cash / 100)}
</Typography>
</Box>
<CustomAvatar />
<IconButton
onClick={handleLogoutClick}
sx={{
ml: "20px",
bgcolor: "#F2F3F7",
borderRadius: "6px",
height: "36px",
width: "36px",
}}
>
<LogoutIcon />
</IconButton>
</Box>
</Container>
) : (
<>
<SectionWrapper
component="nav"
maxWidth="lg"
outerContainerSx={{
backgroundColor: theme.palette.lightPurple.main,
borderBottom: "1px solid #E3E3E3",
}}
sx={{ sx={{
fontSize: "12px", px: "20px",
lineHeight: "14px", display: "flex",
color: theme.palette.grey3.main, justifyContent: "space-between",
height: "80px",
alignItems: "center",
gap: "50px",
}} }}
> >
Мой баланс <PenaLogo width={150} />
</Typography> <Menu />
<Typography variant="body2" color={theme.palette.brightPurple.main}> <Button
{currencyFormatter.format(cash / 100)} component={Link}
</Typography> to={user ? "/tariffs" : "/signin"}
</Box> state={user ? undefined : { backgroundLocation: location }}
<CustomAvatar /> variant="outlined"
<IconButton sx={{
onClick={handleLogoutClick} px: "18px",
sx={{ py: "10px",
ml: "20px", borderColor: "white",
bgcolor: "#F2F3F7", borderRadius: "8px",
borderRadius: "6px", whiteSpace: "nowrap",
height: "36px", minWidth: "180px",
width: "36px", }}
}} >
> Личный кабинет
<LogoutIcon /> </Button>
</IconButton> </SectionWrapper>
</Box> </>
</Container> )}
) : ( <Box>{children}</Box>
<> </Box>
<SectionWrapper
component="nav"
maxWidth="lg"
outerContainerSx={{
backgroundColor: theme.palette.lightPurple.main,
borderBottom: "1px solid #E3E3E3",
}}
sx={{
px: "20px",
display: "flex",
justifyContent: "space-between",
height: "80px",
alignItems: "center",
gap: "50px",
}}
>
<PenaLogo width={150} />
<Menu />
<Button
component={Link}
to={user ? "/tariffs" : "/signin"}
state={user ? undefined : { backgroundLocation: location }}
variant="outlined"
sx={{
px: "18px",
py: "10px",
borderColor: "white",
borderRadius: "8px",
whiteSpace: "nowrap",
minWidth: "180px",
}}
>
Личный кабинет
</Button>
</SectionWrapper>
</>
); );
} }

@ -0,0 +1,118 @@
import {
Popover,
List,
ListItem,
Typography,
useTheme,
useMediaQuery,
} from "@mui/material";
type Notification = {
text: string;
date: string;
watched?: boolean;
};
type NotificationsModalProps = {
open: boolean;
setOpen: (isOpen: boolean) => void;
anchorElement: Element | null;
notifications: Notification[];
};
export const NotificationsModal = ({
open,
setOpen,
anchorElement,
notifications,
}: NotificationsModalProps) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(650));
return (
<Popover
open={open}
anchorEl={anchorElement}
onClose={() => setOpen(false)}
anchorOrigin={{ vertical: "bottom", horizontal: "left" }}
transformOrigin={{ vertical: "top", horizontal: "right" }}
sx={{
"& .MuiPopover-paper": {
maxWidth: isMobile ? "calc(100vw - 30px)" : 600,
width: "100%",
maxHeight: "310px",
borderRadius: "8px",
boxShadow:
"0px 3px 18px rgba(49, 28, 77, 0.1), 0px 3px 34px rgba(49, 28, 77, 0.15)",
"&::-webkit-scrollbar": { width: "6px" },
"&::-webkit-scrollbar-track": {
background: "#F0F0F6",
margin: "5px",
borderRadius: "5px",
},
"&::-webkit-scrollbar-thumb": {
width: "4px",
background: "#9A9AAF",
borderRadius: "5px",
},
},
}}
>
<List sx={{ width: "100%", padding: "5px" }}>
{notifications.map(({ text, date, watched = true }) => (
<ListItem
sx={{
display: "flex",
alignItems: isMobile ? "normal" : "center",
justifyContent: "space-between",
flexDirection: isMobile ? "column-reverse" : "unset",
borderBottom: "1px solid #F2F3F7",
padding: "20px 10px",
background: watched ? "none" : "#F0F0F6",
borderRadius: watched ? "0" : "8px",
"&:first-child": {
borderTop: "1px solid #F2F3F7",
},
}}
>
<Typography
sx={{
position: "relative",
fontSize: "16px",
lineHeight: "19px",
paddingLeft: watched ? "0" : "35px",
fontWeight: watched ? "normal" : "bold",
"&::before": {
content: '""',
display: watched ? "none" : "block",
position: "absolute",
left: "10px",
top: isMobile ? "5px" : "50%",
transform: isMobile ? "none" : "translateY(-50%)",
height: "8px",
width: "8px",
borderRadius: "50%",
background: "#7E2AEA",
},
}}
>
{text}
</Typography>
<Typography
sx={{
fontSize: "14px",
lineHeight: "19px",
color: "#9A9AAF",
fontWeight: watched ? "normal" : "bold",
paddingLeft: isMobile ? (watched ? "0" : "35px") : "0",
marginBottom: isMobile ? "5px" : "0",
}}
>
{date}
</Typography>
</ListItem>
))}
</List>
</Popover>
);
};

@ -1,12 +1,18 @@
import { useState } from "react"; import { useState, useRef } from "react";
import { Select as MuiSelect, MenuItem, useTheme } from "@mui/material"; import {
Select as MuiSelect,
MenuItem,
Box,
Typography,
useTheme,
} from "@mui/material";
import classnames from "classnames"; import classnames from "classnames";
import { cardShadow } from "@root/utils/themes/shadow"; import checkIcon from "@root/assets/Icons/check.svg";
import "./select.css"; import "./select.css";
import checkIcon from "@root/assets/Icons/check.svg"; import type { SelectChangeEvent } from "@mui/material";
type SelectProps = { type SelectProps = {
items: string[]; items: string[];
@ -20,36 +26,73 @@ export const Select = ({
setSelectedItem, setSelectedItem,
}: SelectProps) => { }: SelectProps) => {
const [opened, setOpened] = useState<boolean>(false); const [opened, setOpened] = useState<boolean>(false);
const [currentValue, setCurrentValue] = useState<string>(items[selectedItem]);
const ref = useRef<HTMLDivElement | null>(null);
const theme = useTheme(); const theme = useTheme();
const selectItem = (event: SelectChangeEvent<HTMLDivElement>) => {
setCurrentValue(items[Number(event.target.value)]);
setSelectedItem(Number(event.target.value));
};
return ( return (
<MuiSelect <Box>
className="select" <Box
value={selectedItem} sx={{
onChange={(event) => setSelectedItem(Number(event.target.value))} zIndex: 1500,
sx={{ position: "relative",
width: "100%", width: "100%",
color: theme.palette.brightPurple.main, height: "56px",
border: "2px solid #ffffff", padding: "16px 50px 16px 14px",
borderRadius: "30px", color: theme.palette.brightPurple.main,
fontWeight: "bold", border: "2px solid #ffffff",
boxShadow: cardShadow, borderRadius: "30px",
}} background: "#EFF0F5",
open={opened} boxShadow:
onClick={() => setOpened((isOpened) => !isOpened)} "0px 5px 40px #d2d0e194, 0px 2.76726px 8.55082px rgba(210, 208, 225, 0.4)",
IconComponent={() => ( }}
onClick={() => ref.current?.click()}
>
<Typography
sx={{
fontWeight: "bold",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
overflow: "hidden",
}}
>
{currentValue}
</Typography>
<img <img
src={checkIcon} src={checkIcon}
alt="check" alt="check"
style={{
position: "absolute",
top: "50%",
right: "10px",
transform: "translateY(-50%)",
height: "36px",
width: "36px",
}}
className={classnames("select-icon", { opened })} className={classnames("select-icon", { opened })}
/> />
)} </Box>
> <MuiSelect
{items.map((item, index) => ( ref={ref}
<MenuItem key={item + index} value={index}> className="select"
{item} open={opened}
</MenuItem> MenuProps={{ disablePortal: true }}
))} sx={{ width: "100%" }}
</MuiSelect> onChange={selectItem}
onClick={() => setOpened((isOpened) => !isOpened)}
>
{items.map((item, index) => (
<MenuItem key={item + index} value={index} sx={{ padding: "12px" }}>
{item}
</MenuItem>
))}
</MuiSelect>
</Box>
); );
}; };

@ -1,10 +1,3 @@
.select.MuiInputBase-root.MuiOutlinedInput-root {
z-index: 1500;
background: #ebebf2;
box-shadow: 0px 5px 40px rgba(210, 208, 225, 0.58),
0px 2.76726px 8.55082px rgba(210, 208, 225, 0.4);
}
.MuiOutlinedInput-root .MuiOutlinedInput-notchedOutline { .MuiOutlinedInput-root .MuiOutlinedInput-notchedOutline {
border: 0; border: 0;
} }
@ -20,7 +13,16 @@
} }
.MuiPaper-root.MuiMenu-paper { .MuiPaper-root.MuiMenu-paper {
padding-top: 60px; padding-top: 50px;
margin-top: -60px; margin-top: -50px;
border-radius: 28px; border-radius: 28px;
} }
.MuiInputBase-root.MuiOutlinedInput-root {
display: block;
}
.MuiInputBase-root.MuiOutlinedInput-root > div:first-child,
.MuiInputBase-root.MuiOutlinedInput-root .MuiSelect-icon {
display: none;
}

@ -1,24 +0,0 @@
import { Typography } from "@mui/material";
import { useNavigate } from "react-router";
type Props = {
text: string;
};
export default function StepperSquiz({ text }: Props) {
const navigate = useNavigate();
return (
<Typography component="div">
<Typography
onClick={() => navigate("/tariffs")}
component="div"
sx={{ cursor: "pointer", fontWeight: "400px", fontSize: "12px", lineHeight: "14px", marginBottom: "19px" }}
>
Все тарифы
</Typography>
<Typography component="span" sx={{ fontWeight: "400px", fontSize: "12px", color: "#C19AF5", cursor: "default" }}>
{text}
</Typography>
</Typography>
);
}

@ -1,8 +0,0 @@
import React from "react";
interface Props {
style: {width:string,height:string}
}
export default function (props:Props) {
}

@ -1,7 +1,7 @@
import { Box } from "@mui/material"; import { Box } from "@mui/material";
export default function UploadIcon() { export default function SendIcon() {
return ( return (
<Box sx={{ <Box sx={{

@ -5,7 +5,7 @@ interface Props {
bgcolor: string; bgcolor: string;
} }
export default function WalletIcon({ color, bgcolor }: Props) { export default function SendIcon({ color, bgcolor }: Props) {
return ( return (
<Box <Box
sx={{ sx={{
@ -19,7 +19,13 @@ export default function WalletIcon({ color, bgcolor }: Props) {
ml: "8px", ml: "8px",
}} }}
> >
<svg width="22" height="19" viewBox="0 0 22 19" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg
width="22"
height="19"
viewBox="0 0 22 19"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path <path
d="M19.5714 7.29857V2.29857C19.5714 1.50959 18.9318 0.869996 18.1429 0.869996L2.42857 0.869995C1.63959 0.869995 1 1.50959 1 2.29857L1 16.5843C1 17.3733 1.63959 18.0128 2.42857 18.0128L18.1429 18.0128C18.9318 18.0128 19.5714 17.3733 19.5714 16.5843V11.5843M20.901 6.58428H13.8571C12.2792 6.58428 11 7.86347 11 9.44142C11 11.0194 12.2792 12.2986 13.8571 12.2986H20.901C20.9557 12.2986 21 12.2542 21 12.1996V6.68329C21 6.62861 20.9557 6.58428 20.901 6.58428Z" d="M19.5714 7.29857V2.29857C19.5714 1.50959 18.9318 0.869996 18.1429 0.869996L2.42857 0.869995C1.63959 0.869995 1 1.50959 1 2.29857L1 16.5843C1 17.3733 1.63959 18.0128 2.42857 18.0128L18.1429 18.0128C18.9318 18.0128 19.5714 17.3733 19.5714 16.5843V11.5843M20.901 6.58428H13.8571C12.2792 6.58428 11 7.86347 11 9.44142C11 11.0194 12.2792 12.2986 13.8571 12.2986H20.901C20.9557 12.2986 21 12.2542 21 12.1996V6.68329C21 6.62861 20.9557 6.58428 20.901 6.58428Z"
stroke={color} stroke={color}

@ -1,121 +1,134 @@
import { import {
FormControl, FormControl,
IconButton, IconButton,
InputLabel, InputLabel,
SxProps, SxProps,
TextField, TextField,
TextFieldProps, TextFieldProps,
Theme, Theme,
useMediaQuery, useMediaQuery,
useTheme, useTheme,
} from "@mui/material"; } from "@mui/material";
import * as React from 'react'; import * as React from "react";
import InputAdornment from '@mui/material/InputAdornment'; import InputAdornment from "@mui/material/InputAdornment";
import Visibility from '@mui/icons-material/Visibility'; import Visibility from "@mui/icons-material/Visibility";
import VisibilityOff from '@mui/icons-material/VisibilityOff'; import VisibilityOff from "@mui/icons-material/VisibilityOff";
interface Props {
id: string;
label?: string;
bold?: boolean;
gap?: string;
color?: string;
FormInputSx?: SxProps<Theme>;
TextfieldProps: TextFieldProps;
onChange: (e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => void;
}
export default function ({
id,
label,
bold = false,
gap = "10px",
onChange,
TextfieldProps,
color,
FormInputSx,
}: Props) {
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const labelFont = upMd
? bold
? theme.typography.p1
: { ...theme.typography.body1, fontWeight: 500 }
: theme.typography.body2;
const placeholderFont = upMd ? undefined : { fontWeight: 400, fontSize: "16px", lineHeight: "19px" };
const [showPassword, setShowPassword] = React.useState(false); interface Props {
id: string;
label?: string;
bold?: boolean;
gap?: string;
color?: string;
FormInputSx?: SxProps<Theme>;
TextfieldProps: TextFieldProps;
onChange: (
e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>
) => void;
}
const handleClickShowPassword = () => setShowPassword((show) => !show); export default function PasswordInput({
id,
label,
bold = false,
gap = "10px",
onChange,
TextfieldProps,
color,
FormInputSx,
}: Props) {
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const handleMouseDownPassword = (event: React.MouseEvent<HTMLButtonElement>) => { const labelFont = upMd
event.preventDefault(); ? bold
}; ? theme.typography.p1
: { ...theme.typography.body1, fontWeight: 500 }
return ( : theme.typography.body2;
<FormControl const placeholderFont = upMd
fullWidth ? undefined
variant="standard" : { fontWeight: 400, fontSize: "16px", lineHeight: "19px" };
const [showPassword, setShowPassword] = React.useState(false);
const handleClickShowPassword = () => setShowPassword((show) => !show);
const handleMouseDownPassword = (
event: React.MouseEvent<HTMLButtonElement>
) => {
event.preventDefault();
};
return (
<FormControl
fullWidth
variant="standard"
sx={{
gap,
// mt: "10px",
...FormInputSx,
position: "relative",
}}
>
<InputLabel
shrink
htmlFor={id}
sx={{ sx={{
gap, position: "inherit",
// mt: "10px", color: "black",
...FormInputSx, transform: "none",
position: "relative" ...labelFont,
}} }}
> >
<InputLabel {label}
shrink </InputLabel>
htmlFor={id} <TextField
sx={{ {...TextfieldProps}
position: "inherit", fullWidth
id={id}
sx={{
"& .MuiInputBase-root": {
height: "48px",
borderRadius: "8px",
},
}}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
aria-label="toggle password visibility"
onClick={handleClickShowPassword}
onMouseDown={handleMouseDownPassword}
edge="end"
sx={{
position: "absolute",
right: "15px",
top: "5px",
}}
>
{showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</InputAdornment>
),
sx: {
padding: "0px",
border: "1px solid" + theme.palette.grey2.main,
backgroundColor: color,
borderRadius: "8px",
height: "48px",
color: "black", color: "black",
transform: "none", ...placeholderFont,
...labelFont, "& .MuiInputBase-input": {
}} boxSizing: "border-box",
> height: "100%",
{label} padding: "14px",
</InputLabel>
<TextField
{...TextfieldProps}
fullWidth
id={id}
sx={{
"& .MuiInputBase-root": {
height: "48px",
borderRadius: "8px",
}, },
}} },
InputProps={{ }}
endAdornment: ( onChange={onChange}
<InputAdornment position="end"> type={showPassword ? "text" : "password"}
<IconButton />
aria-label="toggle password visibility" </FormControl>
onClick={handleClickShowPassword} );
onMouseDown={handleMouseDownPassword} }
edge="end"
>
{showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</InputAdornment>
),
sx: {
border: "1px solid" + theme.palette.grey2.main,
backgroundColor: color,
borderRadius: "8px",
height: "48px",
py: 0,
color: "black",
...placeholderFont,
},
}}
onChange={onChange}
type={showPassword ? 'text' : 'password'}
/>
</FormControl>
);
}

@ -5,14 +5,7 @@ import {
useMediaQuery, useMediaQuery,
useTheme, useTheme,
} from "@mui/material"; } from "@mui/material";
import ArrowForwardIcon from "@mui/icons-material/ArrowForward";
import CardWithLink from "@components/CardWithLink";
import UnderlinedLink from "@components/UnderlinedLink";
import SectionWrapper from "@components/SectionWrapper";
import card1Image from "@root/assets/landing/card1.png"; import card1Image from "@root/assets/landing/card1.png";
import card2Image from "@root/assets/landing/card2.png";
import card3Image from "@root/assets/landing/card3.png";
import cardImageBig from "@root/assets/landing/card1big.png";
export default function () { export default function () {
const theme = useTheme(); const theme = useTheme();

@ -1,31 +1,30 @@
import {Box, Typography, useMediaQuery, useTheme} from "@mui/material"; import { Box, useMediaQuery, useTheme } from "@mui/material";
import ArrowForwardIcon from "@mui/icons-material/ArrowForward";
import CardWithLink from "@components/CardWithLink"; import CardWithLink from "@components/CardWithLink";
import UnderlinedLink from "@components/UnderlinedLink";
import SectionWrapper from "@components/SectionWrapper";
import card1Image from "@root/assets/landing/card1.png"; import card1Image from "@root/assets/landing/card1.png";
import card2Image from "@root/assets/landing/card2.png";
import card3Image from "@root/assets/landing/card3.png";
import cardImageBig from "@root/assets/landing/card1big.png";
export default function () { export default function () {
const theme = useTheme(); const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));console.log("я узкий") const upMd = useMediaQuery(theme.breakpoints.up("md"));
return <Box sx={{
return (
<Box
sx={{
mt: upMd ? "93px" : "55px", mt: upMd ? "93px" : "55px",
display: "flex", display: "flex",
flexWrap: "wrap", flexWrap: "wrap",
justifyContent: "space-evenly", justifyContent: "space-evenly",
columnGap: "40px", columnGap: "40px",
rowGap: "50px", rowGap: "50px",
backgroundColor: "\"#E6E6EB" backgroundColor: '"#E6E6EB',
}}> }}
<CardWithLink >
headerText="Шаблонизатор" <CardWithLink
text="Текст- это текст, который имеет некоторые характеристики реального письменного текс" headerText="Шаблонизатор"
linkHref="#" text="Текст- это текст, который имеет некоторые характеристики реального письменного текс"
image={card1Image} linkHref="#"
isHighlighted={!upMd} image={card1Image}
/> isHighlighted={!upMd}
/>
</Box> </Box>
} );
}

@ -1,19 +1,6 @@
import { import { Box, Typography, Button, SxProps, Theme } from "@mui/material";
Box,
Typography,
useMediaQuery,
useTheme,
Button,
SxProps,
Theme,
} from "@mui/material";
import ArrowForwardIcon from "@mui/icons-material/ArrowForward"; import ArrowForwardIcon from "@mui/icons-material/ArrowForward";
import CardWithLink from "@components/CardWithLink";
import UnderlinedLink from "@components/UnderlinedLink"; import UnderlinedLink from "@components/UnderlinedLink";
import SectionWrapper from "@components/SectionWrapper";
import card1Image from "@root/assets/landing/card1.png";
import card2Image from "@root/assets/landing/card2.png";
import card3Image from "@root/assets/landing/card3.png";
import cardImageBig from "@root/assets/landing/card1big.png"; import cardImageBig from "@root/assets/landing/card1big.png";
interface Props { interface Props {
@ -22,9 +9,6 @@ interface Props {
} }
export default function ({ light = true, sx }: Props) { export default function ({ light = true, sx }: Props) {
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
return ( return (
<Box <Box
sx={{ sx={{

@ -1,23 +0,0 @@
import { Box } from "@mui/material";
interface Props {
bgcolor: string;
svg: string;
}
export const IconsCreate = ({ bgcolor, svg }: Props) => (
<Box
component="div"
sx={{
bgcolor,
height: "36px",
width: "36px",
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: "6px",
}}
>
<img src={svg} alt="svg" />
</Box>
);

@ -1,4 +1,3 @@
import type { VerificationStatus } from "@root/model/account";
import type { Attachment } from "@root/model/attachment"; import type { Attachment } from "@root/model/attachment";
type File = { type File = {

@ -3,7 +3,6 @@ import SectionWrapper from "@components/SectionWrapper";
import ArrowBackIcon from "@mui/icons-material/ArrowBack"; import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import TotalPrice from "@components/TotalPrice"; import TotalPrice from "@components/TotalPrice";
import CustomWrapper from "./CustomWrapper"; import CustomWrapper from "./CustomWrapper";
import ComplexNavText from "@root/components/ComplexNavText";
import { useCart } from "@root/utils/hooks/useCart"; import { useCart } from "@root/utils/hooks/useCart";
import { useCustomTariffsStore } from "@root/stores/customTariffs"; import { useCustomTariffsStore } from "@root/stores/customTariffs";
@ -29,7 +28,6 @@ export default function Basket() {
mb: upMd ? "70px" : "37px", mb: upMd ? "70px" : "37px",
}} }}
> >
{upMd && <ComplexNavText text1="Все тарифы — " text2="Корзина" />}
<Box <Box
sx={{ sx={{
mt: "20px", mt: "20px",

@ -5,7 +5,6 @@ import { useState } from "react";
import SectionWrapper from "../../components/SectionWrapper"; import SectionWrapper from "../../components/SectionWrapper";
import AccordionWrapper from "./AccordionWrapper"; import AccordionWrapper from "./AccordionWrapper";
import ArrowBackIcon from "@mui/icons-material/ArrowBack"; import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import ComplexNavText from "../../components/ComplexNavText";
import { Tabs } from "@root/components/Tabs"; import { Tabs } from "@root/components/Tabs";
const subPages = [ const subPages = [
@ -31,9 +30,6 @@ export default function Faq() {
mb: upMd ? "70px" : "37px", mb: upMd ? "70px" : "37px",
}} }}
> >
{upMd && (
<ComplexNavText text1="Все тарифы —" text2=" Вопросы и ответы" />
)}
<Box <Box
sx={{ sx={{
mt: "20px", mt: "20px",

@ -8,7 +8,6 @@ import {
} from "@mui/material"; } from "@mui/material";
import ArrowBackIcon from "@mui/icons-material/ArrowBack"; import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import ComplexNavText from "@root/components/ComplexNavText";
import SectionWrapper from "@root/components/SectionWrapper"; import SectionWrapper from "@root/components/SectionWrapper";
import { Select } from "@root/components/Select"; import { Select } from "@root/components/Select";
import { Tabs } from "@root/components/Tabs"; import { Tabs } from "@root/components/Tabs";
@ -32,7 +31,6 @@ export default function History() {
mb: upMd ? "70px" : "37px", mb: upMd ? "70px" : "37px",
}} }}
> >
{upMd && <ComplexNavText text1="Все тарифы — " text2="История" />}
<Box <Box
sx={{ sx={{
mt: "20px", mt: "20px",

@ -1,15 +1,9 @@
import { Box, Typography, useMediaQuery, useTheme } from "@mui/material"; import { Box, Typography, useMediaQuery, useTheme } from "@mui/material";
import ArrowForwardIcon from "@mui/icons-material/ArrowForward"; import ArrowForwardIcon from "@mui/icons-material/ArrowForward";
import CardWithLink from "@components/CardWithLink";
import UnderlinedLink from "@components/UnderlinedLink"; import UnderlinedLink from "@components/UnderlinedLink";
import WideTemplCard from "@components/wideTemplCard"; import WideTemplCard from "@components/wideTemplCard";
import TemplCardPhonePink from "@components/templCardPhonePink"; import TemplCardPhonePink from "@components/templCardPhonePink";
import SectionWrapper from "@components/SectionWrapper"; import SectionWrapper from "@components/SectionWrapper";
import card1Image from "@root/assets/landing/card1.png";
import card2Image from "@root/assets/landing/card2.png";
import card3Image from "@root/assets/landing/card3.png";
import cardImageBig from "@root/assets/landing/card1big.png";
interface Props { interface Props {
templaterOnly?: boolean; templaterOnly?: boolean;

@ -1,8 +1,13 @@
import { Box, IconButton, Typography, useMediaQuery, useTheme } from "@mui/material"; import {
Box,
IconButton,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
import ArrowBackIcon from "@mui/icons-material/ArrowBack"; import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import CustomButton from "@components/CustomButton"; import CustomButton from "@components/CustomButton";
import SectionWrapper from "@components/SectionWrapper"; import SectionWrapper from "@components/SectionWrapper";
import ComplexNavText from "@components/ComplexNavText";
import PaymentMethodCard from "./PaymentMethodCard"; import PaymentMethodCard from "./PaymentMethodCard";
import mastercardLogo from "../../assets/bank-logo/logo-mastercard.png"; import mastercardLogo from "../../assets/bank-logo/logo-mastercard.png";
import visaLogo from "../../assets/bank-logo/logo-visa.png"; import visaLogo from "../../assets/bank-logo/logo-visa.png";
@ -18,185 +23,193 @@ import { enqueueSnackbar } from "notistack";
import { currencyFormatter } from "@root/utils/currencyFormatter"; import { currencyFormatter } from "@root/utils/currencyFormatter";
import { useLocation } from "react-router-dom"; import { useLocation } from "react-router-dom";
const paymentMethods = [ const paymentMethods = [
{ name: "Mastercard", image: mastercardLogo }, { name: "Mastercard", image: mastercardLogo },
{ name: "Visa", image: visaLogo }, { name: "Visa", image: visaLogo },
{ name: "QIWI Кошелек", image: qiwiLogo }, { name: "QIWI Кошелек", image: qiwiLogo },
{ name: "Мир", image: mirLogo }, { name: "Мир", image: mirLogo },
{ name: "Тинькофф", image: tinkoffLogo }, { name: "Тинькофф", image: tinkoffLogo },
] as const; ] as const;
type PaymentMethod = typeof paymentMethods[number]["name"]; type PaymentMethod = (typeof paymentMethods)[number]["name"];
export default function Payment() { export default function Payment() {
const theme = useTheme(); const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md")); const upMd = useMediaQuery(theme.breakpoints.up("md"));
const upSm = useMediaQuery(theme.breakpoints.up("sm")); const upSm = useMediaQuery(theme.breakpoints.up("sm"));
const [selectedPaymentMethod, setSelectedPaymentMethod] = useState<PaymentMethod | null>(null); const [selectedPaymentMethod, setSelectedPaymentMethod] =
const [paymentValueField, setPaymentValueField] = useState<string>("0"); useState<PaymentMethod | null>(null);
const [paymentLink, setPaymentLink] = useState<string>(""); const [paymentValueField, setPaymentValueField] = useState<string>("0");
const location = useLocation(); const [paymentLink, setPaymentLink] = useState<string>("");
const location = useLocation();
const notEnoughMoneyAmount = location.state?.notEnoughMoneyAmount as number ?? 0; const notEnoughMoneyAmount =
(location.state?.notEnoughMoneyAmount as number) ?? 0;
useEffect(() => { useEffect(() => {
setPaymentValueField((notEnoughMoneyAmount / 100).toString()); setPaymentValueField((notEnoughMoneyAmount / 100).toString());
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
const paymentValue = parseFloat(paymentValueField) * 100; useEffect(() => {
setPaymentLink("");
}, [selectedPaymentMethod]);
function handleChoosePaymentClick() { const paymentValue = parseFloat(paymentValueField) * 100;
sendPayment().then(result => {
setPaymentLink(result.link); function handleChoosePaymentClick() {
}).catch(error => { if (Number(paymentValueField) !== 0) {
const message = getMessageFromFetchError(error); sendPayment()
if (message) enqueueSnackbar(message); .then((result) => {
setPaymentLink(result.link);
})
.catch((error) => {
const message = getMessageFromFetchError(error);
if (message) enqueueSnackbar(message);
}); });
} }
}
return ( return (
<SectionWrapper <SectionWrapper
maxWidth="lg" maxWidth="lg"
sx={{ sx={{
mt: "25px", mt: "25px",
mb: "70px", mb: "70px",
}} }}
>
<Box
sx={{
mt: "20px",
mb: "40px",
display: "flex",
gap: "10px",
}}
>
{!upMd && (
<IconButton
sx={{ p: 0, height: "28px", width: "28px", color: "black" }}
>
<ArrowBackIcon />
</IconButton>
)}
<Typography variant="h4">Способ оплаты</Typography>
</Box>
{!upMd && (
<Typography variant="body2" mb="30px">
Выберите способ оплаты
</Typography>
)}
<Box
sx={{
backgroundColor: upMd ? "white" : undefined,
display: "flex",
flexDirection: upMd ? "row" : "column",
borderRadius: "12px",
boxShadow: upMd ? cardShadow : undefined,
}}
>
<Box
sx={{
width: upMd ? "68.5%" : undefined,
p: upMd ? "20px" : undefined,
display: "flex",
flexDirection: upSm ? "row" : "column",
flexWrap: "wrap",
gap: upMd ? "14px" : "20px",
alignContent: "start",
}}
> >
{upMd && <ComplexNavText text1="Все тарифы — " text2="Способ оплаты" />} {paymentMethods.map((method) => (
<Box <PaymentMethodCard
isSelected={selectedPaymentMethod === method.name}
key={method.name}
name={method.name}
image={method.image}
onClick={() => setSelectedPaymentMethod(method.name)}
/>
))}
</Box>
<Box
sx={{
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
alignItems: "start",
color: theme.palette.grey3.main,
width: upMd ? "31.5%" : undefined,
p: upMd ? "20px" : undefined,
pl: upMd ? "33px" : undefined,
mt: upMd ? undefined : "30px",
borderLeft: upMd
? `1px solid ${theme.palette.grey2.main}`
: undefined,
}}
>
<Box
sx={{
display: "flex",
flexDirection: "column",
maxWidth: "85%",
}}
>
{upMd && <Typography mb="56px">Выберите способ оплаты</Typography>}
<Typography mb="20px">К оплате</Typography>
{paymentLink ? (
<Typography
sx={{ sx={{
mt: "20px", fontWeight: 500,
mb: "40px", fontSize: "20px",
display: "flex", lineHeight: "48px",
gap: "10px", mb: "28px",
}} }}
> >
{!upMd && ( {currencyFormatter.format(paymentValue / 100)}
<IconButton sx={{ p: 0, height: "28px", width: "28px", color: "black" }}> </Typography>
<ArrowBackIcon /> ) : (
</IconButton> <InputTextfield
)} TextfieldProps={{
<Typography variant="h4">Способ оплаты</Typography> placeholder: "К оплате",
</Box> value: paymentValueField,
{!upMd && ( type: "number",
<Typography variant="body2" mb="30px"> }}
Выберите способ оплаты onChange={(e) => setPaymentValueField(e.target.value)}
</Typography> id="payment-amount"
gap={upMd ? "16px" : "10px"}
color={"#F2F3F7"}
FormInputSx={{ mb: "28px" }}
/>
)} )}
<Box </Box>
sx={{ {paymentLink ? (
backgroundColor: upMd ? "white" : undefined, <CustomButton
display: "flex", component="a"
flexDirection: upMd ? "row" : "column", href={paymentLink}
borderRadius: "12px", variant={"contained"}
boxShadow: upMd sx={{
? cardShadow borderColor: theme.palette.brightPurple.main,
: undefined, backgroundColor: theme.palette.brightPurple.main,
}} mt: "auto",
}}
> >
<Box Оплатить
sx={{ </CustomButton>
width: upMd ? "68.5%" : undefined, ) : (
p: upMd ? "20px" : undefined, <CustomButton
display: "flex", disabled={!isFinite(paymentValue)}
flexDirection: upSm ? "row" : "column", variant={"outlined"}
flexWrap: "wrap", onClick={handleChoosePaymentClick}
gap: upMd ? "14px" : "20px", sx={{
alignContent: "start", borderColor: theme.palette.brightPurple.main,
}} backgroundColor: "",
> mt: "auto",
{paymentMethods.map(method => }}
<PaymentMethodCard >
isSelected={selectedPaymentMethod === method.name} Выбрать
key={method.name} </CustomButton>
name={method.name} )}
image={method.image} </Box>
onClick={() => setSelectedPaymentMethod(method.name)} </Box>
/> </SectionWrapper>
)} );
</Box>
<Box
sx={{
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
alignItems: "start",
color: theme.palette.grey3.main,
width: upMd ? "31.5%" : undefined,
p: upMd ? "20px" : undefined,
pl: upMd ? "33px" : undefined,
mt: upMd ? undefined : "30px",
borderLeft: upMd ? `1px solid ${theme.palette.grey2.main}` : undefined,
}}
>
<Box
sx={{
display: "flex",
flexDirection: "column",
maxWidth: "85%",
}}
>
{upMd && <Typography mb="56px">Выберите способ оплаты</Typography>}
<Typography mb="20px">К оплате</Typography>
{paymentLink ?
<Typography
sx={{
fontWeight: 500,
fontSize: "20px",
lineHeight: "48px",
mb: "28px",
}}
>
{currencyFormatter.format(paymentValue / 100)}
</Typography>
:
<InputTextfield
TextfieldProps={{
placeholder: "К оплате",
value: paymentValueField,
type: "number",
}}
onChange={e => setPaymentValueField(e.target.value)}
id="payment-amount"
gap={upMd ? "16px" : "10px"}
color={"#F2F3F7"}
FormInputSx={{
mb: "28px",
}}
/>
}
</Box>
{paymentLink ?
<CustomButton
component="a"
href={paymentLink}
variant={"contained"}
sx={{
borderColor: theme.palette.brightPurple.main,
backgroundColor: theme.palette.brightPurple.main,
mt: "auto",
}}
>
Оплатить
</CustomButton>
:
<CustomButton
disabled={!isFinite(paymentValue)}
variant={"outlined"}
onClick={handleChoosePaymentClick}
sx={{
borderColor: theme.palette.brightPurple.main,
backgroundColor: "",
mt: "auto",
}}
>
Выбрать
</CustomButton>
}
</Box>
</Box>
</SectionWrapper>
);
} }

@ -1,10 +1,13 @@
import { IconButton, useMediaQuery, useTheme } from "@mui/material"; import {
import { Box } from "@mui/material"; IconButton,
import { Typography } from "@mui/material"; Box,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
import SectionWrapper from "../../components/SectionWrapper"; import SectionWrapper from "../../components/SectionWrapper";
import AccordionWrapper from "./AccordionWrapper"; import AccordionWrapper from "./AccordionWrapper";
import ArrowBackIcon from "@mui/icons-material/ArrowBack"; import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import ComplexNavText from "../../components/ComplexNavText";
export default function Faq() { export default function Faq() {
const theme = useTheme(); const theme = useTheme();
@ -18,12 +21,6 @@ export default function Faq() {
mb: upMd ? "70px" : "37px", mb: upMd ? "70px" : "37px",
}} }}
> >
{upMd && (
<ComplexNavText
text1="Все тарифы — Кастомный тариф —"
text2="Сохраненные тарифы"
/>
)}
<Box <Box
sx={{ sx={{
mt: "20px", mt: "20px",

@ -1,89 +1,109 @@
import { Typography, Box, useTheme, useMediaQuery, IconButton } from "@mui/material"; import {
Typography,
Box,
useTheme,
useMediaQuery,
IconButton,
} from "@mui/material";
import ArrowBackIcon from "@mui/icons-material/ArrowBack"; import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import { useParams } from "react-router-dom"; import { Link, useParams } from "react-router-dom";
import SectionWrapper from "@components/SectionWrapper"; import SectionWrapper from "@components/SectionWrapper";
import ComplexNavText from "@components/ComplexNavText";
import SupportChat from "./SupportChat"; import SupportChat from "./SupportChat";
import CreateTicket from "./CreateTicket"; import CreateTicket from "./CreateTicket";
import TicketList from "./TicketList/TicketList"; import TicketList from "./TicketList/TicketList";
import { useCallback } from "react"; import { useCallback } from "react";
import { Ticket, getMessageFromFetchError, useToken } from "@frontend/kitui"; import { Ticket, getMessageFromFetchError, useToken } from "@frontend/kitui";
import { updateTickets, setTicketCount, clearTickets, useTicketStore } from "@root/stores/tickets"; import {
updateTickets,
setTicketCount,
clearTickets,
useTicketStore,
} from "@root/stores/tickets";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
import { useSSESubscription, useTickets } from "@frontend/kitui"; import { useSSESubscription, useTickets } from "@frontend/kitui";
export default function Support() { export default function Support() {
const theme = useTheme(); const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md")); const upMd = useMediaQuery(theme.breakpoints.up("md"));
const ticketId = useParams().ticketId; const ticketId = useParams().ticketId;
const ticketApiPage = useTicketStore(state => state.apiPage); const ticketApiPage = useTicketStore((state) => state.apiPage);
const ticketsPerPage = useTicketStore(state => state.ticketsPerPage); const ticketsPerPage = useTicketStore((state) => state.ticketsPerPage);
const token = useToken(); const token = useToken();
const fetchState = useTickets({ const fetchState = useTickets({
url: "https://hub.pena.digital/heruvym/getTickets", url: "https://hub.pena.digital/heruvym/getTickets",
ticketsPerPage, ticketsPerPage,
ticketApiPage, ticketApiPage,
onNewTickets: useCallback(result => { onNewTickets: useCallback((result) => {
if (result.data) updateTickets(result.data); if (result.data) updateTickets(result.data);
setTicketCount(result.count); setTicketCount(result.count);
}, []), }, []),
onError: useCallback((error: Error) => { onError: useCallback((error: Error) => {
const message = getMessageFromFetchError(error); const message = getMessageFromFetchError(error);
if (message) enqueueSnackbar(message); if (message) enqueueSnackbar(message);
}, []) }, []),
}); });
useSSESubscription<Ticket>({ useSSESubscription<Ticket>({
enabled: Boolean(token), enabled: Boolean(token),
url: `https://admin.pena.digital/heruvym/subscribe?Authorization=${token}`, url: `https://admin.pena.digital/heruvym/subscribe?Authorization=${token}`,
onNewData: updateTickets, onNewData: updateTickets,
onDisconnect: useCallback(() => { onDisconnect: useCallback(() => {
clearTickets(); clearTickets();
}, []), }, []),
marker: "ticket" marker: "ticket",
}); });
return ( return (
<SectionWrapper <SectionWrapper
maxWidth="lg" maxWidth="lg"
sx={{ sx={{
pt: upMd ? "25px" : "20px", pt: upMd ? "25px" : "20px",
pb: upMd ? "82px" : "43px", pb: upMd ? "82px" : "20px",
height: "100%", height: "calc(100vh - 51px)",
}} maxHeight: "calc(100vh - 51px)",
}}
>
<Box
sx={{
mt: "20px",
mb: "40px",
display: "flex",
gap: "10px",
}}
>
<Link
to="/support"
style={{
textDecoration: "none",
display: "flex",
alignItems: "center",
columnGap: "10px",
color: theme.palette.common.black,
}}
> >
{upMd && <ComplexNavText text1="Все тарифы — " text2="Запрос в службу техподдержки" />} <IconButton
<Box sx={{ p: 0, height: "28px", width: "28px", color: "black" }}
sx={{ >
mt: "20px", <ArrowBackIcon />
mb: "40px", </IconButton>
display: "flex", <Typography variant="h4">Запрос в службу техподдержки</Typography>
gap: "10px", </Link>
}} </Box>
> {ticketId ? (
{!upMd && ( <SupportChat />
<IconButton sx={{ p: 0, height: "28px", width: "28px", color: "black" }}> ) : (
<ArrowBackIcon /> <Box
</IconButton> sx={{
)} display: "flex",
<Typography variant="h4">Запрос в службу техподдержки</Typography> flexDirection: "column",
</Box> gap: upMd ? "40px" : "60px",
{ticketId ? ( }}
<SupportChat /> >
) : ( <CreateTicket />
<Box <TicketList fetchState={fetchState} />
sx={{ </Box>
display: "flex", )}
flexDirection: "column", </SectionWrapper>
gap: upMd ? "40px" : "60px", );
}}
>
<CreateTicket />
<TicketList fetchState={fetchState} />
</Box>
)}
</SectionWrapper>
);
} }

@ -17,6 +17,7 @@ import { getMessageFromFetchError, useEventListener, useSSESubscription, useTick
export default function SupportChat() { export default function SupportChat() {
const theme = useTheme(); const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md")); const upMd = useMediaQuery(theme.breakpoints.up("md"));
const isMobile = useMediaQuery(theme.breakpoints.up(460));
const [messageField, setMessageField] = useState<string>(""); const [messageField, setMessageField] = useState<string>("");
const tickets = useTicketStore(state => state.tickets); const tickets = useTicketStore(state => state.tickets);
const messages = useMessageStore(state => state.messages); const messages = useMessageStore(state => state.messages);
@ -134,6 +135,7 @@ export default function SupportChat() {
borderRadius: "12px", borderRadius: "12px",
p: upMd ? "20px" : undefined, p: upMd ? "20px" : undefined,
gap: "40px", gap: "40px",
height: !upMd ? `calc(100% - ${isMobile ? 90 : 115}px)` : null,
boxShadow: upMd boxShadow: upMd
? cardShadow ? cardShadow
: undefined, : undefined,

@ -1,20 +1,23 @@
import { Link as RouterLink } from "react-router-dom";
import { useMemo } from "react"; import { useMemo } from "react";
import { Link as RouterLink } from "react-router-dom";
import { Box, Typography, useMediaQuery, useTheme } from "@mui/material"; import { Box, Typography, useMediaQuery, useTheme } from "@mui/material";
import CustomButton from "@components/CustomButton"; import CustomButton from "@components/CustomButton";
import { cardShadow } from "@root/utils/themes/shadow"; import { cardShadow } from "@root/utils/themes/shadow";
interface Props { import ExclamationPointIcon from "@root/assets/Icons/exclamation_point.svg";
name: string;
body: string;
time: string;
ticketId: string;
}
export default function TicketCard({ name, body, time, ticketId }: Props) { import type { Ticket } from "@frontend/kitui";
type TicketCardProps = {
ticket: Ticket;
};
export default function TicketCard({ ticket }: TicketCardProps) {
const theme = useTheme(); const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md")); const upMd = useMediaQuery(theme.breakpoints.up("md"));
const adminReplied = ticket.user !== ticket.top_message.user_id;
const timeText = useMemo( const timeText = useMemo(
() => ( () => (
@ -28,10 +31,10 @@ export default function TicketCard({ name, body, time, ticketId }: Props) {
mb: "5px", mb: "5px",
}} }}
> >
{time} {new Date(ticket.updated_at).toLocaleDateString()}
</Typography> </Typography>
), ),
[theme.palette.grey2.main, time] [theme.palette.grey2.main, ticket]
); );
return ( return (
@ -48,8 +51,22 @@ export default function TicketCard({ name, body, time, ticketId }: Props) {
boxShadow: cardShadow, boxShadow: cardShadow,
}} }}
> >
{!upMd && timeText} {!upMd && <Typography>{timeText}</Typography>}
<Box> <Box>
{adminReplied && (
<Box
sx={{
display: "flex",
alignItems: "center",
columnGap: "10px",
marginTop: !upMd ? "-20px" : null,
marginBottom: "20px",
}}
>
<img src={ExclamationPointIcon} alt="ExclamationPoint" />
<Typography sx={{ color: "#FB5607" }}>Вам ответили</Typography>
</Box>
)}
<Typography <Typography
sx={{ sx={{
mb: "20px", mb: "20px",
@ -58,9 +75,11 @@ export default function TicketCard({ name, body, time, ticketId }: Props) {
fontWeight: 500, fontWeight: 500,
}} }}
> >
{name} {ticket.title}
</Typography>
<Typography color={theme.palette.grey3.main}>
{ticket.top_message.message}
</Typography> </Typography>
<Typography color={theme.palette.grey3.main}>{body}</Typography>
</Box> </Box>
<Box <Box
sx={{ sx={{
@ -75,7 +94,7 @@ export default function TicketCard({ name, body, time, ticketId }: Props) {
<CustomButton <CustomButton
variant="outlined" variant="outlined"
component={RouterLink} component={RouterLink}
to={`/support/${ticketId}`} to={`/support/${ticket.id}`}
sx={{ sx={{
py: "9px", py: "9px",
color: theme.palette.brightPurple.main, color: theme.palette.brightPurple.main,

@ -1,82 +1,91 @@
import { CircularProgress, List, ListItem, Box, useTheme, Pagination } from "@mui/material"; import {
CircularProgress,
List,
ListItem,
Box,
useTheme,
Pagination,
} from "@mui/material";
import TicketCard from "./TicketCard"; import TicketCard from "./TicketCard";
import { setTicketApiPage, useTicketStore } from "@root/stores/tickets"; import { setTicketApiPage, useTicketStore } from "@root/stores/tickets";
import { Ticket } from "@frontend/kitui"; import { Ticket } from "@frontend/kitui";
interface Props { interface Props {
fetchState: "fetching" | "idle" | "all fetched"; fetchState: "fetching" | "idle" | "all fetched";
} }
export default function TicketList({ fetchState }: Props) { export default function TicketList({ fetchState }: Props) {
const theme = useTheme(); const theme = useTheme();
const tickets = useTicketStore(state => state.tickets); const tickets = useTicketStore((state) => state.tickets);
const ticketCount = useTicketStore(state => state.ticketCount); const ticketCount = useTicketStore((state) => state.ticketCount);
const ticketApiPage = useTicketStore(state => state.apiPage); const ticketApiPage = useTicketStore((state) => state.apiPage);
const ticketsPerPage = useTicketStore(state => state.ticketsPerPage); const ticketsPerPage = useTicketStore((state) => state.ticketsPerPage);
const sortedTickets = tickets.sort(sortTicketsByUpdateTime).slice(ticketApiPage * ticketsPerPage, (ticketApiPage + 1) * ticketsPerPage); const sortedTickets = tickets
.sort(sortTicketsByUpdateTime)
return ( .slice(
<Box ticketApiPage * ticketsPerPage,
sx={{ (ticketApiPage + 1) * ticketsPerPage
display: "flex",
gap: "40px",
flexDirection: "column",
}}
>
<List
sx={{
p: 0,
minHeight: "120px",
display: "flex",
flexDirection: "column",
gap: "40px",
opacity: fetchState === "fetching" ? 0.4 : 1,
transitionProperty: "opacity",
transitionDuration: "200ms",
}}
>
{sortedTickets.map((ticket) => (
<ListItem key={ticket.id} disablePadding>
<TicketCard
name={ticket.title}
body={ticket.top_message.message}
time={new Date(ticket.updated_at).toLocaleDateString()}
ticketId={ticket.id}
/>
</ListItem>
))}
{fetchState === "fetching" && (
<Box
sx={{
position: "absolute",
width: "100%",
height: "100%",
minHeight: "120px",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<CircularProgress sx={{ color: theme.palette.brightPurple.main }} size={60} />
</Box>
)}
</List>
{ticketCount > ticketsPerPage &&
<Pagination
count={Math.ceil(ticketCount / ticketsPerPage)}
page={ticketApiPage + 1}
onChange={(e, value) => setTicketApiPage(value - 1)}
sx={{ alignSelf: "center" }}
/>
}
</Box>
); );
return (
<Box
sx={{
display: "flex",
gap: "40px",
flexDirection: "column",
}}
>
<List
sx={{
p: 0,
minHeight: "120px",
display: "flex",
flexDirection: "column",
gap: "40px",
opacity: fetchState === "fetching" ? 0.4 : 1,
transitionProperty: "opacity",
transitionDuration: "200ms",
}}
>
{sortedTickets.map((ticket) => (
<ListItem key={ticket.id} disablePadding>
<TicketCard ticket={ticket} />
</ListItem>
))}
{fetchState === "fetching" && (
<Box
sx={{
position: "absolute",
width: "100%",
height: "100%",
minHeight: "120px",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<CircularProgress
sx={{ color: theme.palette.brightPurple.main }}
size={60}
/>
</Box>
)}
</List>
{ticketCount > ticketsPerPage && (
<Pagination
count={Math.ceil(ticketCount / ticketsPerPage)}
page={ticketApiPage + 1}
onChange={(e, value) => setTicketApiPage(value - 1)}
sx={{ alignSelf: "center" }}
/>
)}
</Box>
);
} }
function sortTicketsByUpdateTime(ticket1: Ticket, ticket2: Ticket) { function sortTicketsByUpdateTime(ticket1: Ticket, ticket2: Ticket) {
const date1 = new Date(ticket1.updated_at).getTime(); const date1 = new Date(ticket1.updated_at).getTime();
const date2 = new Date(ticket2.updated_at).getTime(); const date2 = new Date(ticket2.updated_at).getTime();
return date2 - date1; return date2 - date1;
} }

@ -1,82 +0,0 @@
import { Box, Typography, useMediaQuery, useTheme } from "@mui/material";
import CustomButton from "@root/components/CustomButton";
import { useCustomTariffsStore } from "@root/stores/customTariffs";
import { currencyFormatter } from "@root/utils/currencyFormatter";
export default function Summary() {
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const summaryPriceBeforeDiscountsMap = useCustomTariffsStore(state => state.summaryPriceBeforeDiscountsMap);
const summaryPriceAfterDiscountsMap = useCustomTariffsStore(state => state.summaryPriceAfterDiscountsMap);
const basePrice = Object.values(summaryPriceBeforeDiscountsMap).reduce((a, e) => a + e, 0);
const discountedPrice = Object.values(summaryPriceAfterDiscountsMap).reduce((a, e) => a + e, 0);
return (
<Box sx={{
display: "flex",
flexDirection: upMd ? "row" : "column",
mt: upMd ? "80px" : "70px",
pt: upMd ? "30px" : undefined,
borderTop: upMd ? `1px solid ${theme.palette.grey2.main}` : undefined,
}}>
<Box sx={{
width: upMd ? "68.5%" : undefined,
pr: upMd ? "15%" : undefined,
display: "flex",
flexWrap: "wrap",
flexDirection: "column",
}}>
<Typography variant="h4" mb={upMd ? "18px" : "30px"}>
Итоговая цена
</Typography>
<Typography color={theme.palette.grey3.main}>
Текст-заполнитель это текст, который имеет Текст-заполнитель это текст, который имеет Текст-заполнитель
это текст, который имеет Текст-заполнитель это текст, который имеет Текст-заполнитель
</Typography>
</Box>
<Box sx={{
color: theme.palette.grey3.main,
width: upMd ? "31.5%" : undefined,
pl: upMd ? "33px" : undefined,
}}>
<Box sx={{
display: "flex",
flexDirection: upMd ? "column" : "row",
alignItems: upMd ? "start" : "center",
mt: upMd ? "10px" : "30px",
gap: "15px",
}}>
<Typography
variant="oldPrice"
sx={{ order: upMd ? 1 : 2 }}
>
{currencyFormatter.format(basePrice / 100)} руб.
</Typography>
<Typography
variant="price"
sx={{
fontWeight: 500,
fontSize: "26px",
lineHeight: "31px",
order: upMd ? 2 : 1,
}}
>
{currencyFormatter.format(discountedPrice / 100)} руб.
</Typography>
</Box>
<CustomButton
variant="contained"
sx={{
mt: "25px",
backgroundColor: theme.palette.brightPurple.main,
}}
>
Выбрать
</CustomButton>
</Box>
</Box>
);
}

@ -1,7 +1,6 @@
import { Box, IconButton, useMediaQuery, useTheme } from "@mui/material"; import { Box, IconButton, useMediaQuery, useTheme } from "@mui/material";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import SectionWrapper from "@components/SectionWrapper"; import SectionWrapper from "@components/SectionWrapper";
import ComplexNavText from "@root/components/ComplexNavText";
import { useCustomTariffsStore } from "@root/stores/customTariffs"; import { useCustomTariffsStore } from "@root/stores/customTariffs";
import ComplexHeader from "@root/components/ComplexHeader"; import ComplexHeader from "@root/components/ComplexHeader";
import CustomTariffCard from "./CustomTariffCard"; import CustomTariffCard from "./CustomTariffCard";
@ -39,7 +38,6 @@ export default function TariffConstructor() {
mb: upMd ? "93px" : "48px", mb: upMd ? "93px" : "48px",
}} }}
> >
{upMd && <ComplexNavText text1="Все тарифы — " text2="Кастомный тариф" />}
<Box <Box
sx={{ sx={{
mt: "20px", mt: "20px",

@ -1,60 +0,0 @@
import { Box, SxProps, Theme, Typography, useTheme } from "@mui/material";
import CustomButton from "@root/components/CustomButton";
interface Props {
image?: string;
headerText: string;
text: string;
linkHref: string;
sx?: SxProps<Theme>;
}
export default function ImageTextButtonCard({ image, headerText, text, linkHref, sx }: Props) {
const theme = useTheme();
return (
<Box sx={{
display: "flex",
flexDirection: "column",
flexGrow: 1,
flexBasis: "300px",
alignItems: "start",
p: "20px",
maxWidth: "360px",
backgroundColor: "#E6E6EB",
border: "1px solid #DBDBDB",
borderRadius: "12px",
boxShadow: "0 10px 0 -5px #BABBC8",
...sx,
}}>
{image &&
<img
src={image}
alt=""
style={{
objectFit: "contain",
width: "100%",
display: "block",
marginBottom: "-10px",
}}
/>
}
<Typography variant="h5">{headerText}</Typography>
<Typography mt="15px" mb="27px" maxWidth="90%">{text}</Typography>
<CustomButton
variant="contained"
sx={{
backgroundColor: "white",
color: theme.palette.primary.main,
mt: "auto",
"&:hover": {
backgroundColor: "#dddddd",
}
}}
>
Подробнее
</CustomButton>
</Box >
);
}

@ -96,12 +96,13 @@ export default function TariffCard({
<Tooltip key={index} title={line} placement="top"> <Tooltip key={index} title={line} placement="top">
<Typography <Typography
sx={{ sx={{
height: "42px", height: "65px",
overflow: "hidden", overflow: "hidden",
textOverflow: "ellipsis", textOverflow: "ellipsis",
display: "-webkit-box", display: "-webkit-box",
WebkitBoxOrient: "vertical", WebkitBoxOrient: "vertical",
WebkitLineClamp: 2, MozBoxOrient: "vertical",
WebkitLineClamp: 3,
}} }}
> >
{line} {line}
@ -115,12 +116,13 @@ export default function TariffCard({
sx={{ sx={{
minHeight: "calc(1.185*2em)", minHeight: "calc(1.185*2em)",
marginBottom: "auto", marginBottom: "auto",
height: "42px", height: "65px",
overflow: "hidden", overflow: "hidden",
textOverflow: "ellipsis", textOverflow: "ellipsis",
display: "-webkit-box", display: "-webkit-box",
WebkitBoxOrient: "vertical", WebkitBoxOrient: "vertical",
WebkitLineClamp: 2, MozBoxOrient: "vertical",
WebkitLineClamp: 3,
}} }}
> >
{text} {text}
@ -134,7 +136,7 @@ export default function TariffCard({
sx={{ sx={{
color: theme.palette.brightPurple.main, color: theme.palette.brightPurple.main,
borderColor: theme.palette.brightPurple.main, borderColor: theme.palette.brightPurple.main,
mt: "35px", mt: "10px",
...buttonProps.sx, ...buttonProps.sx,
}} }}
> >

@ -1,93 +0,0 @@
import { Box, SxProps, Theme, useTheme } from "@mui/material";
import Typography from "@mui/material/Typography";
import CustomButton from "@components/CustomButton";
interface Props {
icon: React.ReactNode;
headerText: string;
text: string;
money?: number;
sx: SxProps<Theme>;
href: string;
buttonBorderColor?: string;
buttonTextColor?: string;
moneyColor?: string;
onclick: () => void;
textButton: string;
}
export default function TariffCardTimeAndVolume({
icon,
headerText,
text,
sx,
href,
buttonBorderColor,
buttonTextColor,
money = 0,
moneyColor,
onclick,
textButton,
}: Props) {
const theme = useTheme();
return (
<Box
component="div"
sx={{
maxWidth: "360px",
width: "360px",
bgcolor: "white",
borderRadius: "12px",
display: "flex",
flexDirection: "column",
alignItems: "start",
p: "20px",
...sx,
}}
>
<Box
component="div"
sx={{
display: "flex",
justifyContent: "space-around",
alignItems: "center",
width: "100%",
}}
>
<Typography component="div">{icon}</Typography>
<Typography
component="div"
variant="h5"
sx={{
display: "flex",
justifyContent: "right",
width: "100%",
color: `${moneyColor ? moneyColor : "#4D4D4D"}`,
}}
>
{money} руб.
</Typography>
</Box>
<Typography variant="h5" sx={{ mt: "14px", mb: "10px" }}>
{headerText}
</Typography>
<Typography component="div" sx={{ minHeight: "calc(1.185*2em)" }}>
{text}
</Typography>
<CustomButton
onClick={onclick}
variant="outlined"
sx={{
color: `${buttonTextColor ? buttonTextColor : theme.palette.brightPurple.main}`,
borderColor: `${buttonBorderColor ? buttonBorderColor : theme.palette.brightPurple.main}`,
mt: "33px",
}}
>
{textButton}
</CustomButton>
</Box>
);
}

@ -1,123 +1,109 @@
import { Outlet, Route, Routes, useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useMediaQuery, useTheme, Box, Link, Typography } from "@mui/material"; import { useMediaQuery, useTheme, Box, Link, Typography } from "@mui/material";
import SectionWrapper from "@components/SectionWrapper"; import SectionWrapper from "@components/SectionWrapper";
import CustomIcon from "@components/icons/CustomIcon"; import CustomIcon from "@components/icons/CustomIcon";
import CalendarIcon from "@components/icons/CalendarIcon"; import CalendarIcon from "@components/icons/CalendarIcon";
import PieChartIcon from "@components/icons/PieChartIcon"; import PieChartIcon from "@components/icons/PieChartIcon";
import TariffCard from "./TariffCard"; import TariffCard from "./TariffCard";
import card1Image from "@root/assets/landing/card1.png";
import card2Image from "@root/assets/landing/card2.png";
import card3Image from "@root/assets/landing/card3.png";
import ImageTextButtonCard from "./ImageTextButtonCard";
import WideTemplCard from "@components/wideTemplCard"; import WideTemplCard from "@components/wideTemplCard";
import TemplCardPhoneLight from "@components/templCardPhoneLight"; import TemplCardPhoneLight from "@components/templCardPhoneLight";
export default function Tariffs() { export default function Tariffs() {
const theme = useTheme(); const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md")); const upMd = useMediaQuery(theme.breakpoints.up("md"));
const navigate = useNavigate(); const navigate = useNavigate();
return ( return (
<SectionWrapper <SectionWrapper
maxWidth="lg" maxWidth="lg"
sx={{ sx={{
mt: upMd ? "60px" : "20px", mt: upMd ? "60px" : "20px",
mb: upMd ? "90px" : "75px", mb: upMd ? "90px" : "75px",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
}} }}
>
<Typography variant="h4">Выберите удобный тариф</Typography>
<Box
sx={{
width: upMd ? "100%" : undefined,
mt: "40px",
mb: "30px",
display: "grid",
flexWrap: "wrap",
justifyContent: "center",
gap: upMd ? "40px" : "30px",
gridTemplateColumns: "repeat(auto-fit, minmax(300px, 360px))",
}}
>
<TariffCard
icon={
<CalendarIcon
color="white"
bgcolor={theme.palette.brightPurple.main}
/>
}
headerText="Тарифы на время"
text="безлимит на 1 месяц , 3 , 6 , 12"
buttonProps={{
text: "Подробнее",
onClick: () => navigate("time"),
}}
sx={{ maxWidth: "360px" }}
/>
<TariffCard
icon={
<PieChartIcon
color="white"
bgcolor={theme.palette.brightPurple.main}
/>
}
headerText="Тариф на объем"
text="200 шаблонов, 1000 шаблонов, 5000 шаблонов, 10 000 шаблонов"
buttonProps={{
text: "Подробнее",
onClick: () => navigate("volume"),
}}
sx={{ maxWidth: "360px" }}
/>
<TariffCard
icon={
<CustomIcon
color="white"
bgcolor={theme.palette.brightPurple.main}
/>
}
headerText="Кастом"
text="Текст-заполнитель — это текст, который имеет "
buttonProps={{
text: "Подробнее",
onClick: () => navigate("/tariffconstructor"),
}}
sx={{ maxWidth: "360px" }}
/>
</Box>
<Typography component="div">
{`Или попробуйте наш `}
<Link
href="#"
sx={{
color: theme.palette.brightPurple.main,
textUnderlinePosition: "under",
textDecorationColor: theme.palette.brightPurple.main,
}}
> >
<Typography variant="h4">Выберите удобный тариф</Typography> бесплатный план
<Box </Link>
sx={{ </Typography>
width: upMd ? "100%" : undefined, <Typography variant="h4" sx={{ mt: upMd ? "60px" : "70px" }}>
mt: "40px", Наши продукты
mb: "30px", </Typography>
display: "flex",
flexWrap: "wrap",
justifyContent: "space-evenly",
gap: upMd ? "40px" : "30px",
}}
>
<TariffCard
icon={<CalendarIcon color="white" bgcolor={theme.palette.brightPurple.main} />}
headerText="Тарифы на время"
text="безлимит на 1 месяц , 3 , 6 , 12"
buttonProps={{
text: "Подробнее",
onClick: () => navigate("time")
}}
sx={{ maxWidth: "360px" }}
/>
<TariffCard
icon={<PieChartIcon color="white" bgcolor={theme.palette.brightPurple.main} />}
headerText="Тариф на объем"
text="200 шаблонов, 1000 шаблонов, 5000 шаблонов, 10 000 шаблонов"
buttonProps={{
text: "Подробнее",
onClick: () => navigate("volume")
}}
sx={{ maxWidth: "360px" }}
/>
<TariffCard
icon={<CustomIcon color="white" bgcolor={theme.palette.brightPurple.main} />}
headerText="Кастом"
text="Текст-заполнитель — это текст, который имеет "
buttonProps={{
text: "Подробнее",
onClick: () => navigate("/tariffconstructor")
}}
sx={{ maxWidth: "360px" }}
/>
</Box>
<Typography component="div">
{`Или попробуйте наш `}
<Link
href="#"
sx={{
color: theme.palette.brightPurple.main,
textUnderlinePosition: "under",
textDecorationColor: theme.palette.brightPurple.main,
}}
>
бесплатный план
</Link>
</Typography>
<Typography variant="h4" sx={{ mt: upMd ? "60px" : "70px" }}>
Наши продукты
</Typography>
{upMd ? {upMd ? (
<WideTemplCard sx={{ marginTop: "60px" }} /> <WideTemplCard sx={{ marginTop: "60px" }} />
: ) : (
<TemplCardPhoneLight />} <TemplCardPhoneLight />
{/*<Box sx={{*/} )}
{/* mt: upMd ? "55px" : "40px",*/} </SectionWrapper>
{/* display: "flex",*/} );
{/* flexWrap: "wrap",*/}
{/* justifyContent: "space-evenly",*/}
{/* gap: upMd ? "40px" : "30px",*/}
{/*}}>*/}
{/* <ImageTextButtonCard*/}
{/* headerText="Шаблонизатор"*/}
{/* text="Текст-заполнитель — это текст, который имеет некоторые характеристики реального письменного текста, но является "*/}
{/* linkHref="#"*/}
{/* image={card1Image}*/}
{/* />*/}
{/* <ImageTextButtonCard*/}
{/* headerText="Опросник"*/}
{/* text="Текст-заполнитель — это текст, который имеет некоторые характеристики реального письменного текста, но является "*/}
{/* linkHref="#"*/}
{/* image={card2Image}*/}
{/* />*/}
{/* <ImageTextButtonCard*/}
{/* headerText="Сокращатель ссылок"*/}
{/* text="Текст-заполнитель — это текст, который имеет некоторые характеристики реального письменного текста, но является "*/}
{/* linkHref="#"*/}
{/* image={card3Image}*/}
{/* />*/}
{/*</Box>*/}
</SectionWrapper>
);
} }

@ -2,7 +2,6 @@ import { useState } from "react";
import { useLocation } from "react-router-dom"; import { useLocation } from "react-router-dom";
import { Box, Typography, useMediaQuery, useTheme } from "@mui/material"; import { Box, Typography, useMediaQuery, useTheme } from "@mui/material";
import SectionWrapper from "@components/SectionWrapper"; import SectionWrapper from "@components/SectionWrapper";
import ComplexNavText from "@root/components/ComplexNavText";
import { useTariffs } from "@root/utils/hooks/useTariffs"; import { useTariffs } from "@root/utils/hooks/useTariffs";
import { updateTariffs, useTariffStore } from "@root/stores/tariffs"; import { updateTariffs, useTariffStore } from "@root/stores/tariffs";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
@ -128,9 +127,6 @@ export default function TariffPage() {
flexDirection: "column", flexDirection: "column",
}} }}
> >
{upMd && (
<ComplexNavText text1="Все тарифы — " text2={StepperText[unit]} />
)}
<Typography variant="h4" sx={{ marginBottom: "23px", mt: "20px" }}> <Typography variant="h4" sx={{ marginBottom: "23px", mt: "20px" }}>
{StepperText[unit]} {StepperText[unit]}
</Typography> </Typography>
@ -149,6 +145,7 @@ export default function TariffPage() {
)} )}
<Box <Box
sx={{ sx={{
justifyContent: "center",
mt: "40px", mt: "40px",
mb: "30px", mb: "30px",
display: "grid", display: "grid",

@ -4,7 +4,6 @@ import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import CustomButton from "@components/CustomButton"; import CustomButton from "@components/CustomButton";
import WalletIcon from "@components/icons/WalletIcon"; import WalletIcon from "@components/icons/WalletIcon";
import SectionWrapper from "@components/SectionWrapper"; import SectionWrapper from "@components/SectionWrapper";
import ComplexNavText from "@components/ComplexNavText";
import { cardShadow } from "@root/utils/themes/shadow"; import { cardShadow } from "@root/utils/themes/shadow";
import { currencyFormatter } from "@root/utils/currencyFormatter"; import { currencyFormatter } from "@root/utils/currencyFormatter";
import { useUserStore } from "@root/stores/user"; import { useUserStore } from "@root/stores/user";
@ -58,7 +57,6 @@ export default function Wallet() {
mb: "70px", mb: "70px",
}} }}
> >
{upMd && <ComplexNavText text1="Все тарифы — " text2="Мой кошелёк" />}
<Box <Box
sx={{ sx={{
mt: "20px", mt: "20px",

@ -656,6 +656,7 @@ const templategenTariff1: Tariff = {
isCustom: false, isCustom: false,
privilegies: [ privilegies: [
{ {
_id: "p1",
name: "n1", name: "n1",
privilegeId: "p1", privilegeId: "p1",
serviceKey: "templategen", serviceKey: "templategen",
@ -678,6 +679,7 @@ const templategenTariff2: Tariff = {
isCustom: false, isCustom: false,
privilegies: [ privilegies: [
{ {
_id: "p5",
name: "n5", name: "n5",
privilegeId: "p5", privilegeId: "p5",
serviceKey: "templategen", serviceKey: "templategen",
@ -700,6 +702,7 @@ const squizTariff: Tariff = {
isCustom: false, isCustom: false,
privilegies: [ privilegies: [
{ {
_id: "p2",
name: "n2", name: "n2",
privilegeId: "p2", privilegeId: "p2",
serviceKey: "squiz", serviceKey: "squiz",
@ -722,6 +725,7 @@ const reducerTariff: Tariff = {
isCustom: false, isCustom: false,
privilegies: [ privilegies: [
{ {
_id: "p3",
name: "n3", name: "n3",
privilegeId: "p3", privilegeId: "p3",
serviceKey: "reducer", serviceKey: "reducer",

@ -1495,10 +1495,10 @@
minimatch "^3.1.2" minimatch "^3.1.2"
strip-json-comments "^3.1.1" strip-json-comments "^3.1.1"
"@frontend/kitui@^1.0.16": "@frontend/kitui@^1.0.17":
version "1.0.16" version "1.0.17"
resolved "https://penahub.gitlab.yandexcloud.net/api/v4/projects/21/packages/npm/@frontend/kitui/-/@frontend/kitui-1.0.16.tgz#bd3f9912d02a983a30a985c7d8732df7bfb3390d" resolved "https://penahub.gitlab.yandexcloud.net/api/v4/projects/21/packages/npm/@frontend/kitui/-/@frontend/kitui-1.0.17.tgz#a5bddaaa18b168be0e1814d5cfbd86e4030d15af"
integrity sha1-vT+ZEtAqmDowqYXH2HMt97+zOQ0= integrity sha1-pb3aqhixaL4OGBTVz72G5AMNFa8=
dependencies: dependencies:
immer "^10.0.2" immer "^10.0.2"
reconnecting-eventsource "^1.6.2" reconnecting-eventsource "^1.6.2"