Merge branch 'deploy' into 'main'

fix: design

See merge request frontend/marketplace!20
This commit is contained in:
Nastya 2023-07-28 16:37:39 +00:00
commit b572f162a0
23 changed files with 961 additions and 785 deletions

@ -1,26 +1,69 @@
import { Slider, SliderProps, styled } from "@mui/material"; import { useState, useEffect } from "react";
import { Slider, useTheme } from "@mui/material";
type CustomSliderProps = {
value: number;
min: number;
max: number;
onChange: (value: number | number[]) => void;
};
export default styled(Slider)<SliderProps>(({ theme }) => ({ export const CustomSlider = ({
color: theme.palette.brightPurple.main, value,
height: "12px", min = 0,
"& .MuiSlider-track": { max = 100,
border: "none", onChange,
}, }: CustomSliderProps) => {
"& .MuiSlider-rail": { const theme = useTheme();
backgroundColor: "#F2F3F7", const [step, setStep] = useState<number>(1);
border: `1px solid ${theme.palette.grey2.main}`,
}, useEffect(() => {
"& .MuiSlider-thumb": { if (value < 100) {
height: 32, return setStep(10);
width: 32, }
border: `6px solid ${theme.palette.brightPurple.main}`,
backgroundColor: "white", if (value < 500) {
boxShadow: `0px 0px 0px 3px white, return setStep(20);
0px 4px 4px 3px #C3C8DD`, }
"&:focus, &:hover, &.Mui-active, &.Mui-focusVisible": {
boxShadow: `0px 0px 0px 3px white, if (value < 2000) {
0px 4px 4px 3px #C3C8DD`, return setStep(50);
}
setStep(150);
}, [value]);
return (
<Slider
value={value}
defaultValue={0}
min={min}
max={max}
step={step}
onChange={(_, newValue) => onChange(newValue)}
sx={{
color: theme.palette.brightPurple.main,
height: "12px",
"& .MuiSlider-track": {
border: "none",
}, },
}, "& .MuiSlider-rail": {
})); backgroundColor: "#F2F3F7",
border: `1px solid ${theme.palette.grey2.main}`,
},
"& .MuiSlider-thumb": {
height: 32,
width: 32,
border: `6px solid ${theme.palette.brightPurple.main}`,
backgroundColor: "white",
boxShadow: `0px 0px 0px 3px white,
0px 4px 4px 3px #C3C8DD`,
"&:focus, &:hover, &.Mui-active, &.Mui-focusVisible": {
boxShadow: `0px 0px 0px 3px white,
0px 4px 4px 3px #C3C8DD`,
},
},
}}
/>
);
};

@ -2,7 +2,6 @@ import { useState } from "react";
import { Box, SvgIcon, Typography, useMediaQuery, useTheme } from "@mui/material"; import { Box, SvgIcon, Typography, useMediaQuery, useTheme } from "@mui/material";
import ClearIcon from "@mui/icons-material/Clear"; import ClearIcon from "@mui/icons-material/Clear";
import { cardShadow } from "@root/utils/themes/shadow";
import { currencyFormatter } from "@root/utils/currencyFormatter"; import { currencyFormatter } from "@root/utils/currencyFormatter";
import { removeTariffFromCart } from "@root/stores/user"; import { removeTariffFromCart } from "@root/stores/user";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
@ -35,7 +34,6 @@ export default function CustomWrapperDrawer({ serviceData }: Props) {
sx={{ sx={{
overflow: "hidden", overflow: "hidden",
borderRadius: "12px", borderRadius: "12px",
boxShadow: cardShadow,
}} }}
> >
<Box <Box

@ -25,12 +25,9 @@ import {
useCartStore, useCartStore,
} 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";
type DrawersProps = { export default function Drawers() {
cartItemsAmount?: number;
};
export default function Drawers({ cartItemsAmount = 0 }: DrawersProps) {
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"));
@ -42,6 +39,7 @@ export default function Drawers({ cartItemsAmount = 0 }: DrawersProps) {
const summaryPriceAfterDiscountsMap = useCustomTariffsStore( const summaryPriceAfterDiscountsMap = useCustomTariffsStore(
(state) => state.summaryPriceAfterDiscountsMap (state) => state.summaryPriceAfterDiscountsMap
); );
const userAccount = useUserStore((state) => state.userAccount);
const basePrice = Object.values(summaryPriceBeforeDiscountsMap).reduce( const basePrice = Object.values(summaryPriceBeforeDiscountsMap).reduce(
(a, e) => a + e, (a, e) => a + e,
@ -74,7 +72,7 @@ export default function Drawers({ cartItemsAmount = 0 }: DrawersProps) {
}} }}
> >
<Badge <Badge
badgeContent={cartItemsAmount} badgeContent={userAccount?.cart.length}
sx={{ sx={{
"& .MuiBadge-badge": { "& .MuiBadge-badge": {
color: "#FFFFFF", color: "#FFFFFF",
@ -132,8 +130,8 @@ export default function Drawers({ cartItemsAmount = 0 }: DrawersProps) {
<Box <Box
sx={{ sx={{
width: "100%", width: "100%",
pt: "20px", pt: "12px",
pb: "20px", pb: "12px",
display: "flex", display: "flex",
justifyContent: "space-between", justifyContent: "space-between",
bgcolor: "#F2F3F7", bgcolor: "#F2F3F7",
@ -202,7 +200,6 @@ export default function Drawers({ cartItemsAmount = 0 }: DrawersProps) {
color: theme.palette.grey3.main, color: theme.palette.grey3.main,
pb: "100px", pb: "100px",
pt: "38px", pt: "38px",
pl: upMd ? "20px" : undefined,
}} }}
> >
<Box <Box

@ -152,7 +152,7 @@ export default function Chat({ sx }: Props) {
<Box sx={{ <Box sx={{
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
height: "clamp(250px, 100dvh - 90px, 600px)", height: "clamp(250px, calc(100vh - 90px), 600px)",
backgroundColor: "#944FEE", backgroundColor: "#944FEE",
borderRadius: "8px", borderRadius: "8px",
...sx, ...sx,

@ -28,6 +28,7 @@ export default function Menu() {
}, },
{ name: "Вопросы и ответы", url: "/faq" }, { name: "Вопросы и ответы", url: "/faq" },
{ name: "Корзина", url: "/basket" }, { name: "Корзина", url: "/basket" },
{ name: "История", url: "/history" },
]; ];
return ( return (

@ -42,6 +42,7 @@ const arrayMenu: MenuItem[] = [
}, },
{ name: "Вопросы и ответы", url: "/faq" }, { name: "Вопросы и ответы", url: "/faq" },
{ name: "Корзина", url: "/basket" }, { name: "Корзина", url: "/basket" },
{ name: "История", url: "/history" },
]; ];
const Transition = React.forwardRef(function Transition( const Transition = React.forwardRef(function Transition(

@ -1,25 +1,23 @@
import { useState } from "react"; import { useState } from "react";
import { Badge, IconButton, useTheme } from "@mui/material"; import { Badge, IconButton, useTheme } from "@mui/material";
import MenuIcon from "@mui/icons-material/Menu"; import MenuIcon from "@mui/icons-material/Menu";
import { Link } from "react-router-dom";
import SectionWrapper from "../SectionWrapper"; import SectionWrapper from "../SectionWrapper";
import { useUserStore } from "@root/stores/user";
import PenaLogo from "../PenaLogo"; import PenaLogo from "../PenaLogo";
import DialogMenu from "./DialogMenu"; import DialogMenu from "./DialogMenu";
import { Link } from "react-router-dom";
import cartIcon from "@root/assets/Icons/cart.svg"; import cartIcon from "@root/assets/Icons/cart.svg";
interface Props { interface Props {
isLoggedIn: boolean; isLoggedIn: boolean;
cartItemsAmount?: number;
} }
export default function NavbarCollapsed({ export default function NavbarCollapsed({ isLoggedIn }: Props) {
isLoggedIn,
cartItemsAmount = 5,
}: Props) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const userAccount = useUserStore((state) => state.userAccount);
const theme = useTheme(); const theme = useTheme();
@ -73,7 +71,7 @@ export default function NavbarCollapsed({
}} }}
> >
<Badge <Badge
badgeContent={cartItemsAmount} badgeContent={userAccount?.cart.length}
sx={{ sx={{
"& .MuiBadge-badge": { "& .MuiBadge-badge": {
color: "#FFFFFF", color: "#FFFFFF",

@ -70,7 +70,7 @@ export default function NavbarFull({ isLoggedIn }: Props) {
ml: "auto", ml: "auto",
}} }}
> >
<Drawers cartItemsAmount={3} /> <Drawers />
<IconButton <IconButton
sx={{ p: 0, ml: "8px" }} sx={{ p: 0, ml: "8px" }}
onClick={() => navigate("/wallet")} onClick={() => navigate("/wallet")}

@ -1,87 +1,98 @@
import { InputAdornment, TextField, Typography, useTheme } from "@mui/material";
import { useState } from "react"; import { useState } from "react";
import { InputAdornment, TextField, Typography, useTheme } from "@mui/material";
import type { ChangeEvent } from "react";
interface Props { interface Props {
id: string; id: string;
adornmentText: string; adornmentText: string;
onChange: (value: number) => void; onChange: (value: number) => void;
} }
export default function NumberInputWithUnitAdornment({ id, adornmentText, onChange }: Props) { export default function NumberInputWithUnitAdornment({
const theme = useTheme(); id,
const [valueField, setValueField] = useState<string>(""); adornmentText,
onChange,
}: Props) {
const theme = useTheme();
const [valueField, setValueField] = useState<string>("");
return ( return (
<TextField <TextField
type="number" type="number"
size="small" size="small"
placeholder="Введите вручную" placeholder="Введите вручную"
id={id} id={id}
value={valueField} value={valueField}
onChange={e => { onChange={({ target }: ChangeEvent<HTMLInputElement>) => {
let n = parseInt(e.target.value); const newNumber = parseInt(target.value);
if (!isFinite(n)) n = 0; if (!isFinite(newNumber) || newNumber < 0) {
onChange(0);
setValueField(String(0));
onChange(n); return;
setValueField(n.toString()); }
}}
onChange(newNumber);
setValueField(String(newNumber));
}}
sx={{
maxWidth: "200px",
minWidth: "200px",
".MuiInputBase-root": {
pr: 0,
height: "48px",
borderRadius: "8px",
backgroundColor: "#F2F3F7",
fieldset: {
border: "1px solid" + theme.palette.grey2.main,
},
"&.Mui-focused fieldset": {
borderColor: theme.palette.brightPurple.main,
},
input: {
height: "31px",
borderRight: !valueField ? "none" : "1px solid #9A9AAF",
},
"&.Mui-focused input": {
borderRight: "1px solid #9A9AAF",
},
"&:not(.Mui-focused) .MuiInputAdornment-root": {
display: !valueField ? "none" : undefined,
},
"&.Mui-focused ::-webkit-input-placeholder": {
color: "transparent",
},
// Hiding arrows
"input::-webkit-outer-spin-button, input::-webkit-inner-spin-button":
{
WebkitAppearance: "none",
margin: 0,
},
"input[type = number]": {
MozAppearance: "textfield",
},
},
}}
InputProps={{
endAdornment: (
<InputAdornment
position="end"
sx={{ sx={{
maxWidth: "200px", userSelect: "none",
minWidth: "200px", pointerEvents: "none",
".MuiInputBase-root": { pl: "2px",
pr: 0, pr: "13px",
height: "48px",
borderRadius: "8px",
backgroundColor: "#F2F3F7",
"fieldset": {
border: "1px solid" + theme.palette.grey2.main,
},
"&.Mui-focused fieldset": {
borderColor: theme.palette.brightPurple.main,
},
"input": {
height: "31px",
borderRight: !valueField ? "none" : "1px solid #9A9AAF",
},
"&.Mui-focused input": {
borderRight: "1px solid #9A9AAF",
},
"&:not(.Mui-focused) .MuiInputAdornment-root": {
display: !valueField ? "none" : undefined,
},
"&.Mui-focused ::-webkit-input-placeholder": {
color: "transparent",
},
// Hiding arrows
"input::-webkit-outer-spin-button, input::-webkit-inner-spin-button": {
"WebkitAppearance": "none",
margin: 0,
},
"input[type = number]": {
"MozAppearance": "textfield",
}
},
}} }}
InputProps={{ >
endAdornment: ( <Typography variant="body2" color="#4D4D4D">
<InputAdornment {adornmentText}
position="end" </Typography>
sx={{ </InputAdornment>
userSelect: "none", ),
pointerEvents: "none", }}
pl: "2px", />
pr: "13px", );
}}
>
<Typography variant="body2" color="#4D4D4D">
{adornmentText}
</Typography>
</InputAdornment>
)
}}
/>
);
} }

@ -1,4 +1,10 @@
import {Box, Button, Typography, useMediaQuery, useTheme} from "@mui/material"; import {
Box,
Button,
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 CardWithLink from "@components/CardWithLink";
import UnderlinedLink from "@components/UnderlinedLink"; import UnderlinedLink from "@components/UnderlinedLink";
@ -9,73 +15,76 @@ import card3Image from "@root/assets/landing/card3.png";
import cardImageBig from "@root/assets/landing/card1big.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={{ console.log("я узкий");
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: "inherit" backgroundColor: "inherit",
}}> }}
<Box >
sx={{ <Box
display: "flex", sx={{
flexDirection: "column", display: "flex",
flexGrow: 1, flexDirection: "column",
alignItems: "start", flexGrow: 1,
p: "20px", alignItems: "start",
maxWidth: "360px", p: "20px",
backgroundColor: " #E6E6EB", maxWidth: "360px",
borderRadius: "12px", backgroundColor: " #E6E6EB",
boxShadow: ` borderRadius: "12px",
0px 100px 309px rgba(37, 39, 52, 0.24), boxShadow: "0 10px 0 -5px #BABBC8",
0px 41.7776px 129.093px rgba(37, 39, 52, 0.172525), color: "black",
0px 22.3363px 69.0192px rgba(37, 39, 52, 0.143066), height: "520px",
0px 12.5216px 38.6916px rgba(37, 39, 52, 0.12), justifyContent: "space-between",
0px 6.6501px 20.5488px rgba(37, 39, 52, 0.0969343), }}
0px 2.76726px 8.55082px rgba(37, 39, 52, 0.0674749) >
`, <img
color: "black", src={card1Image}
height: "520px", alt=""
justifyContent: "space-between" style={{
}} objectFit: "contain",
width: "100%",
display: "block",
pointerEvents: "none",
}}
/>
<Typography variant="h5">Шаблонизатор</Typography>
<Typography mt="20px" mb="20px">
"Текст- это текст, который имеет некоторые характеристики реального
письменного текст"
</Typography>
<Button
sx={{
width: "180px",
paddingTop: "10px",
paddingBottom: "10px",
borderRadius: "8px",
boxShadow: "none",
backgroundColor: "white",
color: "black",
"&:hover": {
backgroundColor: "#581CA7",
color: "white",
},
"&:active": {
backgroundColor: "black",
color: "white",
},
}}
variant="contained"
> >
<img Подробнее
src={card1Image} </Button>
alt="" </Box>
style={{
objectFit: "contain",
width: "100%",
display: "block",
pointerEvents: "none",
}}
/>
<Typography variant="h5">Шаблонизатор</Typography>
<Typography mt="20px" mb="20px">"Текст- это текст, который имеет некоторые характеристики реального письменного текст"</Typography>
<Button
sx={{
width: "180px",
paddingTop: "10px",
paddingBottom: "10px",
borderRadius: "8px",
boxShadow: "none",
backgroundColor: "white",
color: "black",
"&:hover": {
backgroundColor: "#581CA7",
color: "white"
},
"&:active": {
backgroundColor: "black",
color: "white"
}
}}
variant="contained">Подробнее</Button>
</Box>
</Box> </Box>
} );
}

@ -1,4 +1,12 @@
import {Box, Typography, useMediaQuery, useTheme, Button, SxProps, Theme} from "@mui/material"; import {
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 CardWithLink from "@components/CardWithLink";
import UnderlinedLink from "@components/UnderlinedLink"; import UnderlinedLink from "@components/UnderlinedLink";
@ -9,15 +17,17 @@ 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 {
light?: boolean; light?: boolean;
sx?: SxProps<Theme>; sx?: SxProps<Theme>;
} }
export default function ({light = true, sx}: Props) { export default function ({ light = true, sx }: Props) {
const theme = useTheme(); const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md")); const upMd = useMediaQuery(theme.breakpoints.up("md"));
return <Box sx={{ return (
<Box
sx={{
position: "relative", position: "relative",
display: "flex", display: "flex",
justifyContent: "space-between", justifyContent: "space-between",
@ -25,68 +35,70 @@ export default function ({light = true, sx}: Props) {
px: "20px", px: "20px",
backgroundColor: light ? "#E6E6EB" : "#434657", backgroundColor: light ? "#E6E6EB" : "#434657",
borderRadius: "12px", borderRadius: "12px",
boxShadow: ` boxShadow: "0 10px 0 -5px #BABBC8",
0px 100px 309px rgba(37, 39, 52, 0.24), ...sx,
0px 41.7776px 129.093px rgba(37, 39, 52, 0.172525), }}
0px 22.3363px 69.0192px rgba(37, 39, 52, 0.143066), >
0px 12.5216px 38.6916px rgba(37, 39, 52, 0.12), <Box
0px 6.6501px 20.5488px rgba(37, 39, 52, 0.0969343), sx={{
0px 2.76726px 8.55082px rgba(37, 39, 52, 0.0674749) display: "flex",
`, flexDirection: "column",
...sx }}
}}> >
<Box sx={{ <Typography variant="h5">Шаблонизатор</Typography>
display: "flex", <Typography mt="20px" maxWidth="552px">
flexDirection: "column", Текст- это текст, который имеет некоторые характеристики реального
}}> письменного текс
<Typography variant="h5">Шаблонизатор</Typography> </Typography>
<Typography mt="20px" maxWidth="552px">Текст- это текст, который имеет некоторые характеристики {light ? (
реального <Button
письменного текс</Typography> sx={{
{ mt: "28px",
light ? width: "180px",
<Button paddingTop: "10px",
sx={{ paddingBottom: "10px",
mt:"28px", borderRadius: "8px",
width: "180px", boxShadow: "none",
paddingTop: "10px", backgroundColor: "white",
paddingBottom: "10px", color: "black",
borderRadius: "8px", "&:hover": {
boxShadow: "none", backgroundColor: "#581CA7",
backgroundColor: "white", color: "white",
color: "black", },
"&:hover": { "&:active": {
backgroundColor: "#581CA7", backgroundColor: "black",
color: "white" color: "white",
}, },
"&:active": {
backgroundColor: "black",
color: "white"
}
}}
variant="contained">Подробнее</Button>
:
<UnderlinedLink
linkHref="#"
text="Подробнее"
endIcon={<ArrowForwardIcon sx={{height: "20px", width: "20px"}}/>}
sx={{
mt: "auto",
}}
/>
}
</Box>
<img
src={cardImageBig}
alt=""
style={{
display: "block",
objectFit: "contain",
pointerEvents: "none",
marginBottom: "-40px",
marginTop: "-110px",
maxWidth: "390px",
}} }}
/> variant="contained"
>
Подробнее
</Button>
) : (
<UnderlinedLink
linkHref="#"
text="Подробнее"
endIcon={
<ArrowForwardIcon sx={{ height: "20px", width: "20px" }} />
}
sx={{
mt: "auto",
}}
/>
)}
</Box>
<img
src={cardImageBig}
alt=""
style={{
display: "block",
objectFit: "contain",
pointerEvents: "none",
marginBottom: "-40px",
marginTop: "-110px",
maxWidth: "390px",
}}
/>
</Box> </Box>
} );
}

@ -1,5 +1,11 @@
import { useState } from "react"; import { useState } from "react";
import { Box, SvgIcon, Typography, useMediaQuery, useTheme } from "@mui/material"; import {
Box,
SvgIcon,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
import ExpandIcon from "@components/icons/ExpandIcon"; import ExpandIcon from "@components/icons/ExpandIcon";
import ClearIcon from "@mui/icons-material/Clear"; import ClearIcon from "@mui/icons-material/Clear";
import { cardShadow } from "@root/utils/themes/shadow"; import { cardShadow } from "@root/utils/themes/shadow";
@ -8,168 +14,183 @@ import { removeTariffFromCart } from "@root/stores/user";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
import { ServiceCartData, getMessageFromFetchError } from "@frontend/kitui"; import { ServiceCartData, getMessageFromFetchError } from "@frontend/kitui";
const name: Record<string, string> = {
const name: Record<string, string> = { templategen: "Шаблонизатор", squiz: "Опросник", reducer: "Сокращатель ссылок" }; templategen: "Шаблонизатор",
squiz: "Опросник",
reducer: "Сокращатель ссылок",
};
interface Props { interface Props {
serviceData: ServiceCartData; serviceData: ServiceCartData;
} }
export default function CustomWrapper({ serviceData }: Props) { export default function CustomWrapper({ serviceData }: Props) {
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 [isExpanded, setIsExpanded] = useState<boolean>(false); const [isExpanded, setIsExpanded] = useState<boolean>(false);
function handleItemDeleteClick(tariffId: string) { function handleItemDeleteClick(tariffId: string) {
removeTariffFromCart(tariffId).then(() => { removeTariffFromCart(tariffId)
enqueueSnackbar("Тариф удален"); .then(() => {
}).catch(error => { enqueueSnackbar("Тариф удален");
const message = getMessageFromFetchError(error); })
if (message) enqueueSnackbar(message); .catch((error) => {
}); const message = getMessageFromFetchError(error);
} if (message) enqueueSnackbar(message);
});
}
return ( return (
<Box
sx={{
overflow: "hidden",
borderRadius: "12px",
boxShadow: cardShadow,
}}
>
<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 <Box
sx={{ onClick={() => setIsExpanded((prev) => !prev)}
overflow: "hidden", sx={{
borderRadius: "12px", height: "72px",
boxShadow: cardShadow, px: "20px",
}}
display: "flex",
alignItems: "center",
justifyContent: "space-between",
cursor: "pointer",
userSelect: "none",
}}
> >
<Box <Typography
sx={{ sx={{
backgroundColor: "white", fontSize: upMd ? "20px" : "16px",
"&:first-of-type": { lineHeight: upMd ? undefined : "19px",
borderTopLeftRadius: "12px", fontWeight: 500,
borderTopRightRadius: "12px", color: theme.palette.text.secondary,
}, px: 0,
"&:last-of-type": { }}
borderBottomLeftRadius: "12px", >
borderBottomRightRadius: "12px", {name[serviceData.serviceKey]}
}, </Typography>
"&:not(:last-of-type)": {
borderBottom: `1px solid ${theme.palette.grey2.main}`, <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,
}}
> >
<Box {currencyFormatter.format(serviceData.price / 100)}
onClick={() => setIsExpanded((prev) => !prev)} </Typography>
sx={{ <Box
height: "72px", sx={{
px: "20px", borderLeft: upSm ? "1px solid #9A9AAF" : "none",
paddingLeft: upSm ? "24px" : 0,
display: "flex", height: "100%",
alignItems: "center", display: "flex",
justifyContent: "space-between", justifyContent: "center",
cursor: "pointer", alignItems: "center",
userSelect: "none", }}
}} >
> <ExpandIcon isExpanded={isExpanded} />
<Typography
sx={{
fontSize: upMd ? "20px" : "16px",
lineHeight: upMd ? undefined : "19px",
fontWeight: 500,
color: theme.palette.text.secondary,
px: 0,
}}
>
{name[serviceData.serviceKey]}
</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 }}>
{currencyFormatter.format(serviceData.price / 100)}
</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 &&
serviceData.privileges.map(privilege => (
<Box
key={privilege.tariffId + privilege.privilegeId}
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,
}}
>
{privilege.description}
</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,
}}
>
{currencyFormatter.format(privilege.price / 100)}
</Typography>
{upSm ? (
<Typography
component="div"
onClick={() => handleItemDeleteClick(privilege.tariffId)}
sx={{
color: theme.palette.text.secondary,
borderBottom: `1px solid ${theme.palette.text.secondary}`,
width: "max-content",
lineHeight: "19px",
cursor: "pointer",
}}
>
Удалить
</Typography>
) : (
<SvgIcon onClick={() => handleItemDeleteClick(privilege.tariffId)} component={ClearIcon}></SvgIcon>
)}
</Box>
</Box>
))}
</Box> </Box>
</Box>
</Box> </Box>
); {isExpanded &&
serviceData.privileges.map((privilege) => (
<Box
key={privilege.tariffId + privilege.privilegeId}
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,
}}
>
{privilege.description}
</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,
}}
>
{currencyFormatter.format(privilege.price / 100)}
</Typography>
{upSm ? (
<Typography
component="div"
onClick={() => handleItemDeleteClick(privilege.tariffId)}
sx={{
color: theme.palette.text.secondary,
borderBottom: `1px solid ${theme.palette.text.secondary}`,
width: "max-content",
lineHeight: "19px",
cursor: "pointer",
}}
>
Удалить
</Typography>
) : (
<SvgIcon
onClick={() => handleItemDeleteClick(privilege.tariffId)}
component={ClearIcon}
sx={{ fill: "#7E2AEA" }}
/>
)}
</Box>
</Box>
))}
</Box>
</Box>
);
} }

@ -37,7 +37,7 @@ export default function Faq() {
<Box <Box
sx={{ sx={{
mt: "20px", mt: "20px",
mb: upMd ? "40px" : "20px", mb: "20px",
display: "flex", display: "flex",
gap: "10px", gap: "10px",
}} }}

@ -36,7 +36,7 @@ export default function History() {
<Box <Box
sx={{ sx={{
mt: "20px", mt: "20px",
mb: upMd ? "40px" : "20px", mb: "20px",
display: "flex", display: "flex",
gap: "10px", gap: "10px",
}} }}

@ -11,88 +11,94 @@ import SectionWrapper from "@components/SectionWrapper";
import ArrowForwardIcon from "@mui/icons-material/ArrowForward"; import ArrowForwardIcon from "@mui/icons-material/ArrowForward";
export default function Section3() { export default function Section3() {
const theme = useTheme(); const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md")); const upMd = useMediaQuery(theme.breakpoints.up("md"));
const downXs = useMediaQuery(theme.breakpoints.down("sm")); const downXs = useMediaQuery(theme.breakpoints.down("sm"));
return ( return (
<SectionWrapper <SectionWrapper
component="section" component="section"
maxWidth="lg" maxWidth="lg"
outerContainerSx={{ outerContainerSx={{
backgroundColor: theme.palette.lightPurple.main, backgroundColor: theme.palette.lightPurple.main,
}} }}
sx={{ sx={{
display: "flex", display: "flex",
pt: upMd ? "170px" : "155px", pt: upMd ? "170px" : "155px",
pb: upMd ? "100px" : "70px", pb: upMd ? "100px" : "70px",
// width: "fit-content", // width: "fit-content",
margin: "auto", margin: "auto",
flexDirection: upMd ? "row" : "column", flexDirection: upMd ? "row" : "column",
flexWrap: "wrap", flexWrap: "wrap",
rowGap: upMd ? "58px" : "30px", rowGap: upMd ? "58px" : "30px",
columnGap: "13.8%", columnGap: "13.8%",
justifyContent: "space-between", justifyContent: "space-between",
}} }}
>
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "start",
maxWidth: "500px",
width: upMd ? "43.1%" : undefined,
mb: "10px",
}}
>
<Typography
variant="h4"
sx={{
mb: upMd ? "70px" : "30px",
}}
> >
<Box sx={{ Узнайте, как наши сервисы решают ваши задачи
display: "flex", </Typography>
flexDirection: "column", <Box
alignItems: "start", sx={{
maxWidth: "500px", mb: upMd ? "20px" : "30px",
width: upMd ? "43.1%" : undefined, }}
mb: "10px", >
}}> <Typography>Покажут эффективность рекламы</Typography>
<Typography <Typography>Соберут все обращения клиентов</Typography>
variant="h4" <Typography>Повысят конверсию сайта</Typography>
sx={{ </Box>
mb: upMd ? "70px" : "30px", <UnderlinedLink
}} linkHref="#"
> text="Подробнее"
Узнайте, как наши сервисы решают ваши задачи endIcon={
</Typography> <ArrowForwardIcon
<Box sx={{ height: "20px", width: "20px", display: "inline" }}
sx={{
mb: upMd ? "20px" : "30px",
}}
>
<Typography>Покажут эффективность рекламы</Typography>
<Typography>Соберут все обращения клиентов</Typography>
<Typography>Повысят конверсию сайта</Typography>
</Box>
<UnderlinedLink
linkHref="#"
text="Подробнее"
endIcon={<ArrowForwardIcon sx={{ height: "20px", width: "20px", display: "inline" }} />}
/>
</Box>
<PromoCard
width={upMd ? "43.1%" : "100%"}
headerText="Общий кабинет"
text="Текст-заполнитель — это текст, который имеет некоторые характеристики реального письменного"
textOrientation="column"
small={downXs}
backgroundImage={downXs ? cardPagesBackground4 : cardPagesBackground1}
sx={{ alignSelf: "center" }}
/> />
<PromoCard }
width={upMd ? "43.1%" : "100%"} />
headerText="Общий кабинет" </Box>
text="Текст-заполнитель — это текст, который имеет некоторые характеристики реального письменного" <PromoCard
textOrientation="row" width={upMd ? "43.1%" : "100%"}
small={downXs} headerText="Общий кабинет"
backgroundImage={downXs ? cardPagesBackground5 : cardPagesBackground2} text="Текст-заполнитель — это текст, который имеет некоторые характеристики реального письменного"
sx={{ alignSelf: "center" }} textOrientation="column"
/> small={downXs}
<PromoCard backgroundImage={downXs ? cardPagesBackground4 : cardPagesBackground1}
width={upMd ? "43.1%" : "100%"} sx={{ alignSelf: "center" }}
headerText="Гибкие тарифы" />
text="Текст-заполнитель — это текст, который имеет некоторые характеристики реального письменного" <PromoCard
textOrientation="column" width={upMd ? "43.1%" : "100%"}
small={downXs} headerText="Общий кабинет"
backgroundImage={downXs ? cardPagesBackground6 : cardPagesBackground3} text="Текст-заполнитель — это текст, который имеет некоторые характеристики реального письменного"
sx={{ mt: upMd ? "82px" : undefined, alignSelf: "center" }} textOrientation="row"
/> small={downXs}
</SectionWrapper> backgroundImage={downXs ? cardPagesBackground5 : cardPagesBackground2}
); sx={{ alignSelf: "center", mt: upMd ? "-82px" : null }}
} />
<PromoCard
width={upMd ? "43.1%" : "100%"}
headerText="Гибкие тарифы"
text="Текст-заполнитель — это текст, который имеет некоторые характеристики реального письменного"
textOrientation="column"
small={downXs}
backgroundImage={downXs ? cardPagesBackground6 : cardPagesBackground3}
sx={{ mt: upMd ? "82px" : undefined, alignSelf: "center" }}
/>
</SectionWrapper>
);
}

@ -1,120 +1,144 @@
import { Box, Divider, Typography, useMediaQuery, useTheme } from "@mui/material"; import {
Box,
Divider,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
import CustomButton from "../../components/CustomButton"; import CustomButton from "../../components/CustomButton";
import { Privilege } from "@root/model/privilege"; import { Privilege } from "@root/model/privilege";
import TariffPrivilegeSlider from "./TariffItem"; import TariffPrivilegeSlider from "./TariffItem";
import { createAndSendTariff, useCustomTariffsStore } from "@root/stores/customTariffs"; import {
createAndSendTariff,
useCustomTariffsStore,
} from "@root/stores/customTariffs";
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 { devlog, getMessageFromFetchError } from "@frontend/kitui"; import { devlog, getMessageFromFetchError } from "@frontend/kitui";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
interface Props { interface Props {
serviceKey: string; serviceKey: string;
privileges: Privilege[]; privileges: Privilege[];
} }
export default function CustomTariffCard({ serviceKey, privileges }: Props) { export default function CustomTariffCard({ serviceKey, privileges }: Props) {
const theme = useTheme(); const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md")); const upMd = useMediaQuery(theme.breakpoints.up("md"));
const summaryPriceBeforeDiscounts = useCustomTariffsStore(state => state.summaryPriceBeforeDiscountsMap); const summaryPriceBeforeDiscounts = useCustomTariffsStore(
const summaryPriceAfterDiscounts = useCustomTariffsStore(state => state.summaryPriceAfterDiscountsMap); (state) => state.summaryPriceBeforeDiscountsMap
);
const summaryPriceAfterDiscounts = useCustomTariffsStore(
(state) => state.summaryPriceAfterDiscountsMap
);
const priceBeforeDiscounts = summaryPriceBeforeDiscounts[serviceKey] ?? 0; const priceBeforeDiscounts = summaryPriceBeforeDiscounts[serviceKey] ?? 0;
const priceAfterDiscounts = summaryPriceAfterDiscounts[serviceKey] ?? 0; const priceAfterDiscounts = summaryPriceAfterDiscounts[serviceKey] ?? 0;
async function handleConfirmClick() { async function handleConfirmClick() {
createAndSendTariff(serviceKey).then(result => { createAndSendTariff(serviceKey)
devlog(result); .then((result) => {
enqueueSnackbar("Тариф создан"); devlog(result);
}).catch(error => { enqueueSnackbar("Тариф создан");
const message = getMessageFromFetchError(error, "Не удалось создать тариф"); })
if (message) enqueueSnackbar(message); .catch((error) => {
}); const message = getMessageFromFetchError(
} error,
"Не удалось создать тариф"
);
if (message) enqueueSnackbar(message);
});
}
return ( return (
<Box sx={{ <Box
backgroundColor: "white", sx={{
width: "100%", backgroundColor: "white",
width: "100%",
display: "flex",
flexDirection: upMd ? "row" : "column",
borderRadius: "12px",
boxShadow: cardShadow,
}}
>
<Box
sx={{
p: "20px",
pr: upMd ? "35px" : undefined,
display: "flex",
flexBasis: 0,
flexGrow: 2.37,
flexWrap: "wrap",
flexDirection: "column",
gap: "25px",
}}
>
{privileges.map((privilege) => (
<TariffPrivilegeSlider key={privilege._id} privilege={privilege} />
))}
</Box>
{!upMd && (
<Divider
sx={{ mx: "20px", my: "10px", borderColor: theme.palette.grey2.main }}
/>
)}
<Box
sx={{
display: "flex",
flexBasis: 0,
flexGrow: 1,
flexDirection: "column",
justifyContent: "space-between",
alignItems: "start",
color: theme.palette.grey3.main,
p: "20px",
pl: upMd ? "33px" : undefined,
borderLeft: upMd
? `1px solid ${theme.palette.grey2.main}`
: undefined,
}}
>
<Box
sx={{
display: "flex", display: "flex",
flexDirection: upMd ? "row" : "column", justifyContent: "space-between",
borderRadius: "12px", gap: "15%",
boxShadow: cardShadow, mb: "auto",
}}> width: "100%",
<Box sx={{ }}
p: "20px", >
pr: upMd ? "35px" : undefined, <Typography>
display: "flex", Чем больше пакеты, тем дешевле подписки и опции{" "}
flexBasis: 0, </Typography>
flexGrow: 2.37,
flexWrap: "wrap",
flexDirection: "column",
gap: "25px",
}}>
{privileges.map(privilege =>
<TariffPrivilegeSlider
key={privilege._id}
privilege={privilege}
/>
)}
</Box>
{!upMd && <Divider sx={{ mx: "20px", my: "10px", borderColor: theme.palette.grey2.main }} />}
<Box sx={{
display: "flex",
flexBasis: 0,
flexGrow: 1,
flexDirection: "column",
justifyContent: "space-between",
alignItems: "start",
color: theme.palette.grey3.main,
p: "20px",
pl: upMd ? "33px" : undefined,
borderLeft: upMd ? `1px solid ${theme.palette.grey2.main}` : undefined,
}}>
<Box sx={{
display: "flex",
justifyContent: "space-between",
gap: "15%",
mb: "auto",
width: "100%",
}}>
<Typography>Чем больше пакеты, тем дешевле подписки и опции </Typography>
<Box sx={{
px: "6.7px",
height: "36px",
color: "white",
backgroundColor: theme.palette.orange.main,
display: "flex",
justifyContent: "center",
alignItems: "center",
borderRadius: "8px",
}}>
{"-60%"}
</Box>
</Box>
<Typography mb="20px" mt="18px">
Сумма с учетом скидки
</Typography>
<Box sx={{
display: "flex",
alignItems: "center",
gap: "20px",
mb: "30px",
}}>
<Typography variant="price">{currencyFormatter.format(priceAfterDiscounts / 100)}</Typography>
<Typography variant="oldPrice" pt="3px">{currencyFormatter.format(priceBeforeDiscounts / 100)}</Typography>
</Box>
<CustomButton
variant="contained"
onClick={handleConfirmClick}
sx={{
backgroundColor: theme.palette.brightPurple.main,
}}
>
Выбрать
</CustomButton>
</Box>
</Box> </Box>
); <Typography mb="20px" mt="18px">
Сумма с учетом скидки
</Typography>
<Box
sx={{
display: "flex",
alignItems: "center",
gap: "20px",
mb: "30px",
}}
>
<Typography variant="price">
{currencyFormatter.format(priceAfterDiscounts / 100)}
</Typography>
<Typography variant="oldPrice" pt="3px">
{currencyFormatter.format(priceBeforeDiscounts / 100)}
</Typography>
</Box>
<CustomButton
variant="contained"
onClick={handleConfirmClick}
sx={{
backgroundColor: theme.palette.brightPurple.main,
}}
>
Выбрать
</CustomButton>
</Box>
</Box>
);
} }

@ -9,82 +9,96 @@ import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import TotalPrice from "@root/components/TotalPrice"; import TotalPrice from "@root/components/TotalPrice";
import { serviceNameByKey } from "@root/utils/serviceKeys"; import { serviceNameByKey } from "@root/utils/serviceKeys";
export default function TariffConstructor() { export default function TariffConstructor() {
const theme = useTheme(); const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md")); const upMd = useMediaQuery(theme.breakpoints.up("md"));
const customTariffs = useCustomTariffsStore(state => state.customTariffsMap); const customTariffs = useCustomTariffsStore(
const summaryPriceBeforeDiscountsMap = useCustomTariffsStore(state => state.summaryPriceBeforeDiscountsMap); (state) => state.customTariffsMap
const summaryPriceAfterDiscountsMap = useCustomTariffsStore(state => state.summaryPriceAfterDiscountsMap); );
const summaryPriceBeforeDiscountsMap = useCustomTariffsStore(
(state) => state.summaryPriceBeforeDiscountsMap
);
const summaryPriceAfterDiscountsMap = useCustomTariffsStore(
(state) => state.summaryPriceAfterDiscountsMap
);
const basePrice = Object.values(summaryPriceBeforeDiscountsMap).reduce((a, e) => a + e, 0); const basePrice = Object.values(summaryPriceBeforeDiscountsMap).reduce(
const discountedPrice = Object.values(summaryPriceAfterDiscountsMap).reduce((a, e) => a + e, 0); (a, e) => a + e,
0
);
const discountedPrice = Object.values(summaryPriceAfterDiscountsMap).reduce(
(a, e) => a + e,
0
);
return ( return (
<SectionWrapper <SectionWrapper
maxWidth="lg" maxWidth="lg"
sx={{ sx={{
mt: upMd ? "25px" : "20px", mt: upMd ? "25px" : "20px",
mb: upMd ? "93px" : "48px", mb: upMd ? "93px" : "48px",
}} }}
> >
{upMd && <ComplexNavText text1="Все тарифы — " text2="Кастомный тариф" />} {upMd && <ComplexNavText text1="Все тарифы — " text2="Кастомный тариф" />}
<Box <Box
sx={{
mt: "20px",
display: "flex",
flexDirection: "column",
gap: "80px",
}}
>
{Object.entries(customTariffs).map(
([serviceKey, privileges], index) => (
<Box key={serviceKey}>
<Box
sx={{ sx={{
mt: "20px", mb: "40px",
display: "flex", display: "flex",
flexDirection: "column", gap: "10px",
gap: "80px",
}} }}
> >
{Object.entries(customTariffs).map(([serviceKey, privileges], index) => ( {!upMd && index === 0 && (
<Box key={serviceKey}> <IconButton
<Box sx={{
sx={{ p: 0,
mb: "40px", height: "28px",
display: "flex", width: "28px",
gap: "10px", color: "black",
}}
>
{!upMd && index === 0 && (
<IconButton
sx={{
p: 0,
height: "28px",
width: "28px",
color: "black",
}}
>
<ArrowBackIcon />
</IconButton>
)}
<ComplexHeader
text1="Кастомный тариф "
text2={serviceNameByKey[serviceKey]}
/>
</Box>
<CustomTariffCard serviceKey={serviceKey} privileges={privileges} />
</Box>
))}
</Box>
{upMd && (
<Link
to="/tariffconstructor/savedtariffs"
style={{
display: "block",
marginTop: "60px",
textUnderlinePosition: "under",
color: theme.palette.brightPurple.main,
textDecorationColor: theme.palette.brightPurple.main,
}} }}
> >
Ваши сохраненные тарифы <ArrowBackIcon />
</Link> </IconButton>
)} )}
<TotalPrice <ComplexHeader
priceBeforeDiscounts={basePrice} text1="Кастомный тариф "
priceAfterDiscounts={discountedPrice} text2={serviceNameByKey[serviceKey]}
/> />
</SectionWrapper> </Box>
); <CustomTariffCard
serviceKey={serviceKey}
privileges={privileges}
/>
</Box>
)
)}
</Box>
<Link
to="/tariffconstructor/savedtariffs"
style={{
display: "block",
marginTop: "60px",
textUnderlinePosition: "under",
color: theme.palette.brightPurple.main,
textDecorationColor: theme.palette.brightPurple.main,
}}
>
Ваши сохраненные тарифы
</Link>
<TotalPrice
priceBeforeDiscounts={basePrice}
priceAfterDiscounts={discountedPrice}
/>
</SectionWrapper>
);
} }

@ -1,135 +1,169 @@
import { useThrottle } from "@frontend/kitui"; import { useThrottle } from "@frontend/kitui";
import { Box, SliderProps, Typography, useMediaQuery, useTheme } from "@mui/material"; import {
import CustomSlider from "@root/components/CustomSlider"; Box,
SliderProps,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
import { CustomSlider } from "@root/components/CustomSlider";
import NumberInputWithUnitAdornment from "@root/components/NumberInputWithUnitAdornment"; import NumberInputWithUnitAdornment from "@root/components/NumberInputWithUnitAdornment";
import CalendarIcon from "@root/components/icons/CalendarIcon"; import CalendarIcon from "@root/components/icons/CalendarIcon";
import PieChartIcon from "@root/components/icons/PieChartIcon"; import PieChartIcon from "@root/components/icons/PieChartIcon";
import { Privilege, PrivilegeValueType } from "@root/model/privilege"; import { Privilege, PrivilegeValueType } from "@root/model/privilege";
import { useCartStore } from "@root/stores/cart"; import { useCartStore } from "@root/stores/cart";
import { setCustomTariffsUserValue, useCustomTariffsStore } from "@root/stores/customTariffs"; import {
setCustomTariffsUserValue,
useCustomTariffsStore,
} from "@root/stores/customTariffs";
import { useDiscountStore } from "@root/stores/discounts"; import { useDiscountStore } from "@root/stores/discounts";
import { useUserStore } from "@root/stores/user"; import { useUserStore } from "@root/stores/user";
import { getDeclension } from "@root/utils/declension"; import { getDeclension } from "@root/utils/declension";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
const sliderSettingsByType: Record<PrivilegeValueType, Partial<SliderProps>> = { const sliderSettingsByType: Record<PrivilegeValueType, Partial<SliderProps>> = {
"день": { день: { max: 365 },
max: 365, шаблон: { max: 1000000 },
step: 1, МБ: { max: 1000000 },
},
"шаблон": {
max: 1000000,
step: 1000,
},
"МБ": {
max: 1000000,
step: 1000,
},
}; };
interface Props { interface Props {
privilege: Privilege; privilege: Privilege;
} }
export default function TariffPrivilegeSlider({ privilege }: Props) { export default function TariffPrivilegeSlider({ privilege }: Props) {
const theme = useTheme(); const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md")); const upMd = useMediaQuery(theme.breakpoints.up("md"));
const userValue = useCustomTariffsStore(state => state.userValuesMap[privilege.serviceKey]?.[privilege._id]) ?? 0; const userValue =
const discounts = useDiscountStore(state => state.discounts); useCustomTariffsStore(
const currentCartTotal = useCartStore(state => state.cart.priceAfterDiscounts); (state) => state.userValuesMap[privilege.serviceKey]?.[privilege._id]
const purchasesAmount = useUserStore(state => state.userAccount?.wallet.purchasesAmount) ?? 0; ) ?? 0;
const [value, setValue] = useState<number>(userValue); const discounts = useDiscountStore((state) => state.discounts);
const throttledValue = useThrottle(value, 200); const currentCartTotal = useCartStore(
(state) => state.cart.priceAfterDiscounts
);
const purchasesAmount =
useUserStore((state) => state.userAccount?.wallet.purchasesAmount) ?? 0;
const [value, setValue] = useState<number>(userValue);
const throttledValue = useThrottle(value, 200);
useEffect(function setStoreValue() { useEffect(
setCustomTariffsUserValue( function setStoreValue() {
privilege.serviceKey, setCustomTariffsUserValue(
privilege._id, privilege.serviceKey,
throttledValue, privilege._id,
discounts, throttledValue,
currentCartTotal, discounts,
purchasesAmount currentCartTotal,
); purchasesAmount
}, [currentCartTotal, discounts, purchasesAmount, privilege._id, privilege.serviceKey, throttledValue]); );
},
[
currentCartTotal,
discounts,
purchasesAmount,
privilege._id,
privilege.serviceKey,
throttledValue,
]
);
function handleSliderChange(event: Event, value: number | number[]) { function handleSliderChange(value: number | number[]) {
if (Array.isArray(value)) throw new Error("Slider uses multiple values instead of one"); if (Array.isArray(value))
throw new Error("Slider uses multiple values instead of one");
setValue(value); setValue(value);
} }
const quantityText = `${value} ${getDeclension(value, privilege.value)}`; const quantityText = `${value} ${getDeclension(value, privilege.value)}`;
const quantityElement = ( const quantityElement = (
<Box sx={{ <Box
sx={{
display: "flex",
gap: "15px",
alignItems: "center",
justifyContent: upMd ? "end" : undefined,
flexWrap: "wrap",
mt: upMd ? undefined : "12px",
}}
>
<Typography
variant="p1"
color={theme.palette.brightPurple.main}
textAlign="end"
>
{quantityText}
</Typography>
<Box
sx={{
display: "flex",
gap: "15px",
alignItems: "center",
flexWrap: "wrap",
}}
>
<Typography sx={{ fontSize: "16px", lineHeight: "19px", mt: "1px" }}>
или
</Typography>
<NumberInputWithUnitAdornment
id={"privilege_input_" + privilege._id}
adornmentText={getDeclension(0, privilege.value)}
onChange={(value) => setValue(value)}
/>
</Box>
</Box>
);
const icon =
privilege.type === "day" ? (
<CalendarIcon color={theme.palette.orange.main} bgcolor="#FEDFD0" />
) : (
<PieChartIcon color={theme.palette.orange.main} bgcolor="#FEDFD0" />
);
return (
<Box>
<Typography sx={{ color: theme.palette.grey3.main, mb: "auto" }}>
{privilege.description}
</Typography>
<Box
sx={{
display: "flex",
flexDirection: "column",
mt: "40px",
}}
>
<Box
sx={{
display: "flex", display: "flex",
gap: "15px", // flexWrap: "wrap",
alignItems: "center", alignItems: "center",
justifyContent: upMd ? "end" : undefined, mb: "8px",
flexWrap: "wrap", justifyContent: "space-between",
mt: upMd ? undefined : "12px", gap: "10px",
}}> }}
<Typography variant="p1" color={theme.palette.brightPurple.main} textAlign="end"> >
{quantityText} <Box
</Typography> sx={{
<Box sx={{ display: "flex",
display: "flex", alignItems: "center",
gap: "15px", gap: "22px",
alignItems: "center", }}
flexWrap: "wrap", >
}}> {icon}
<Typography sx={{ fontSize: "16px", lineHeight: "19px", mt: "1px" }}>или</Typography> <Typography variant="h5">{privilege.name}</Typography>
<NumberInputWithUnitAdornment </Box>
id={"privilege_input_" + privilege._id} {upMd && quantityElement}
adornmentText={getDeclension(0, privilege.value)}
onChange={value => setValue(value)}
/>
</Box>
</Box> </Box>
); <CustomSlider
value={value}
const icon = privilege.type === "day" min={0}
? <CalendarIcon color={theme.palette.orange.main} bgcolor="#FEDFD0" /> max={sliderSettingsByType[privilege.value].max || 100}
: <PieChartIcon color={theme.palette.orange.main} bgcolor="#FEDFD0" />; onChange={handleSliderChange}
/>
return ( {!upMd && quantityElement}
<Box> </Box>
<Typography sx={{ color: theme.palette.grey3.main, mb: "auto" }}> </Box>
{privilege.description} );
</Typography>
<Box sx={{
display: "flex",
flexDirection: "column",
mt: "40px",
}}>
<Box sx={{
display: "flex",
// flexWrap: "wrap",
alignItems: "center",
mb: "8px",
justifyContent: "space-between",
gap: "10px",
}}>
<Box sx={{
display: "flex",
alignItems: "center",
gap: "22px",
}}>
{icon}
<Typography variant="h5">{privilege.name}</Typography>
</Box>
{upMd && quantityElement}
</Box>
<CustomSlider
value={value}
defaultValue={0}
min={0}
onChange={handleSliderChange}
{...sliderSettingsByType[privilege.value]}
/>
{!upMd && quantityElement}
</Box>
</Box>
);
} }

@ -75,7 +75,7 @@ export default function TariffCard({ icon, headerText, text, sx, price, buttonPr
sx={{ sx={{
color: theme.palette.brightPurple.main, color: theme.palette.brightPurple.main,
borderColor: theme.palette.brightPurple.main, borderColor: theme.palette.brightPurple.main,
mt: "auto", mt: "30px",
...buttonProps.sx, ...buttonProps.sx,
}} }}
> >

@ -89,7 +89,7 @@ export default function Tariffs() {
</Typography> </Typography>
{upMd ? {upMd ?
<WideTemplCard sx={{ marginTop: "76px" }} /> <WideTemplCard sx={{ marginTop: "60px" }} />
: :
<TemplCardPhoneLight />} <TemplCardPhoneLight />}
{/*<Box sx={{*/} {/*<Box sx={{*/}

@ -23,17 +23,20 @@ import { useCartStore } from "@root/stores/cart";
const subPages = ["Шаблонизатор", "Опросник", "Сокращатель ссылок"]; const subPages = ["Шаблонизатор", "Опросник", "Сокращатель ссылок"];
export default function TariffPage() { export default function TariffPage() {
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.down(600)); const isMobile = useMediaQuery(theme.breakpoints.down(600));
const location = useLocation(); const location = useLocation();
const tariffs = useTariffStore(state => state.tariffs); const tariffs = useTariffStore((state) => state.tariffs);
const [selectedItem, setSelectedItem] = useState<number>(0); const [selectedItem, setSelectedItem] = useState<number>(0);
const discounts = useDiscountStore(state => state.discounts); const discounts = useDiscountStore((state) => state.discounts);
const customTariffs = useCustomTariffsStore(state => state.customTariffsMap); const customTariffs = useCustomTariffsStore(
const purchasesAmount = useUserStore(state => state.userAccount?.wallet.purchasesAmount) ?? 0; (state) => state.customTariffsMap
const cart = useCartStore(state => state.cart); );
const unit: string = String(location.pathname).slice(9); const purchasesAmount =
useUserStore((state) => state.userAccount?.wallet.purchasesAmount) ?? 0;
const cart = useCartStore((state) => state.cart);
const unit: string = String(location.pathname).slice(9);
const StepperText: Record<string, string> = { const StepperText: Record<string, string> = {
volume: "Тарифы на объём", volume: "Тарифы на объём",
@ -155,7 +158,7 @@ export default function TariffPage() {
> >
{tariffElements} {tariffElements}
</Box> </Box>
<Typography variant="h4" sx={{ mt: "50px", mb: "40px" }}> <Typography variant="h4" sx={{ mt: "40px" }}>
Ранее вы покупали Ранее вы покупали
</Typography> </Typography>
<Slider items={tariffElements} /> <Slider items={tariffElements} />

@ -1,3 +1,7 @@
.slider {
margin-top: 40px;
}
.slider .slick-slide { .slider .slick-slide {
width: 100%; width: 100%;
max-width: 360px; max-width: 360px;

@ -59,7 +59,7 @@ export const Slider = ({ items }: SliderProps) => {
display: "grid", display: "grid",
gap: "40px", gap: "40px",
gridTemplateColumns: "repeat(auto-fit, minmax(300px, 360px))", gridTemplateColumns: "repeat(auto-fit, minmax(300px, 360px))",
margin: isTablet ? "auto" : null, margin: isTablet ? "40px auto" : null,
}} }}
> >
{items} {items}