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,7 +1,47 @@
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 = ({
value,
min = 0,
max = 100,
onChange,
}: CustomSliderProps) => {
const theme = useTheme();
const [step, setStep] = useState<number>(1);
useEffect(() => {
if (value < 100) {
return setStep(10);
}
if (value < 500) {
return setStep(20);
}
if (value < 2000) {
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, color: theme.palette.brightPurple.main,
height: "12px", height: "12px",
"& .MuiSlider-track": { "& .MuiSlider-track": {
@ -23,4 +63,7 @@ export default styled(Slider)<SliderProps>(({ theme }) => ({
0px 4px 4px 3px #C3C8DD`, 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,6 +1,7 @@
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;
@ -8,7 +9,11 @@ interface Props {
onChange: (value: number) => void; onChange: (value: number) => void;
} }
export default function NumberInputWithUnitAdornment({ id, adornmentText, onChange }: Props) { export default function NumberInputWithUnitAdornment({
id,
adornmentText,
onChange,
}: Props) {
const theme = useTheme(); const theme = useTheme();
const [valueField, setValueField] = useState<string>(""); const [valueField, setValueField] = useState<string>("");
@ -19,13 +24,18 @@ export default function NumberInputWithUnitAdornment({ id, adornmentText, onChan
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={{ sx={{
maxWidth: "200px", maxWidth: "200px",
@ -35,13 +45,13 @@ export default function NumberInputWithUnitAdornment({ id, adornmentText, onChan
height: "48px", height: "48px",
borderRadius: "8px", borderRadius: "8px",
backgroundColor: "#F2F3F7", backgroundColor: "#F2F3F7",
"fieldset": { fieldset: {
border: "1px solid" + theme.palette.grey2.main, border: "1px solid" + theme.palette.grey2.main,
}, },
"&.Mui-focused fieldset": { "&.Mui-focused fieldset": {
borderColor: theme.palette.brightPurple.main, borderColor: theme.palette.brightPurple.main,
}, },
"input": { input: {
height: "31px", height: "31px",
borderRight: !valueField ? "none" : "1px solid #9A9AAF", borderRight: !valueField ? "none" : "1px solid #9A9AAF",
}, },
@ -56,13 +66,14 @@ export default function NumberInputWithUnitAdornment({ id, adornmentText, onChan
}, },
// Hiding arrows // Hiding arrows
"input::-webkit-outer-spin-button, input::-webkit-inner-spin-button": { "input::-webkit-outer-spin-button, input::-webkit-inner-spin-button":
"WebkitAppearance": "none", {
WebkitAppearance: "none",
margin: 0, margin: 0,
}, },
"input[type = number]": { "input[type = number]": {
"MozAppearance": "textfield", MozAppearance: "textfield",
} },
}, },
}} }}
InputProps={{ InputProps={{
@ -80,7 +91,7 @@ export default function NumberInputWithUnitAdornment({ id, adornmentText, onChan
{adornmentText} {adornmentText}
</Typography> </Typography>
</InputAdornment> </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";
@ -10,16 +16,20 @@ 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 <Box
sx={{ sx={{
display: "flex", display: "flex",
@ -30,17 +40,10 @@ export default function () {
maxWidth: "360px", maxWidth: "360px",
backgroundColor: " #E6E6EB", backgroundColor: " #E6E6EB",
borderRadius: "12px", borderRadius: "12px",
boxShadow: ` boxShadow: "0 10px 0 -5px #BABBC8",
0px 100px 309px rgba(37, 39, 52, 0.24),
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),
0px 6.6501px 20.5488px rgba(37, 39, 52, 0.0969343),
0px 2.76726px 8.55082px rgba(37, 39, 52, 0.0674749)
`,
color: "black", color: "black",
height: "520px", height: "520px",
justifyContent: "space-between" justifyContent: "space-between",
}} }}
> >
<img <img
@ -54,7 +57,10 @@ export default function () {
}} }}
/> />
<Typography variant="h5">Шаблонизатор</Typography> <Typography variant="h5">Шаблонизатор</Typography>
<Typography mt="20px" mb="20px">"Текст- это текст, который имеет некоторые характеристики реального письменного текст"</Typography> <Typography mt="20px" mb="20px">
"Текст- это текст, который имеет некоторые характеристики реального
письменного текст"
</Typography>
<Button <Button
sx={{ sx={{
@ -67,15 +73,18 @@ export default function () {
color: "black", color: "black",
"&:hover": { "&:hover": {
backgroundColor: "#581CA7", backgroundColor: "#581CA7",
color: "white" color: "white",
}, },
"&:active": { "&:active": {
backgroundColor: "black", backgroundColor: "black",
color: "white" color: "white",
} },
}} }}
variant="contained">Подробнее</Button> variant="contained"
>
Подробнее
</Button>
</Box> </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";
@ -13,11 +21,13 @@ interface Props {
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,29 +35,25 @@ 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)
`,
...sx
}}>
<Box sx={{
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
}}> }}
>
<Typography variant="h5">Шаблонизатор</Typography> <Typography variant="h5">Шаблонизатор</Typography>
<Typography mt="20px" maxWidth="552px">Текст- это текст, который имеет некоторые характеристики <Typography mt="20px" maxWidth="552px">
реального Текст- это текст, который имеет некоторые характеристики реального
письменного текс</Typography> письменного текс
{ </Typography>
light ? {light ? (
<Button <Button
sx={{ sx={{
mt:"28px", mt: "28px",
width: "180px", width: "180px",
paddingTop: "10px", paddingTop: "10px",
paddingBottom: "10px", paddingBottom: "10px",
@ -57,24 +63,29 @@ mt:"28px",
color: "black", color: "black",
"&:hover": { "&:hover": {
backgroundColor: "#581CA7", backgroundColor: "#581CA7",
color: "white" color: "white",
}, },
"&:active": { "&:active": {
backgroundColor: "black", backgroundColor: "black",
color: "white" color: "white",
} },
}} }}
variant="contained">Подробнее</Button> variant="contained"
: >
Подробнее
</Button>
) : (
<UnderlinedLink <UnderlinedLink
linkHref="#" linkHref="#"
text="Подробнее" text="Подробнее"
endIcon={<ArrowForwardIcon sx={{height: "20px", width: "20px"}}/>} endIcon={
<ArrowForwardIcon sx={{ height: "20px", width: "20px" }} />
}
sx={{ sx={{
mt: "auto", mt: "auto",
}} }}
/> />
} )}
</Box> </Box>
<img <img
src={cardImageBig} src={cardImageBig}
@ -89,4 +100,5 @@ mt:"28px",
}} }}
/> />
</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,8 +14,11 @@ 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;
@ -22,9 +31,11 @@ export default function CustomWrapper({ serviceData }: Props) {
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)
.then(() => {
enqueueSnackbar("Тариф удален"); enqueueSnackbar("Тариф удален");
}).catch(error => { })
.catch((error) => {
const message = getMessageFromFetchError(error); const message = getMessageFromFetchError(error);
if (message) enqueueSnackbar(message); if (message) enqueueSnackbar(message);
}); });
@ -88,7 +99,13 @@ export default function CustomWrapper({ serviceData }: Props) {
gap: upSm ? "111px" : "17px", gap: upSm ? "111px" : "17px",
}} }}
> >
<Typography sx={{ color: theme.palette.grey3.main, fontSize: upSm ? "20px" : "16px", fontWeight: 500 }}> <Typography
sx={{
color: theme.palette.grey3.main,
fontSize: upSm ? "20px" : "16px",
fontWeight: 500,
}}
>
{currencyFormatter.format(serviceData.price / 100)} {currencyFormatter.format(serviceData.price / 100)}
</Typography> </Typography>
<Box <Box
@ -106,7 +123,7 @@ export default function CustomWrapper({ serviceData }: Props) {
</Box> </Box>
</Box> </Box>
{isExpanded && {isExpanded &&
serviceData.privileges.map(privilege => ( serviceData.privileges.map((privilege) => (
<Box <Box
key={privilege.tariffId + privilege.privilegeId} key={privilege.tariffId + privilege.privilegeId}
sx={{ sx={{
@ -164,7 +181,11 @@ export default function CustomWrapper({ serviceData }: Props) {
Удалить Удалить
</Typography> </Typography>
) : ( ) : (
<SvgIcon onClick={() => handleItemDeleteClick(privilege.tariffId)} component={ClearIcon}></SvgIcon> <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",
}} }}

@ -35,14 +35,16 @@ export default function Section3() {
justifyContent: "space-between", justifyContent: "space-between",
}} }}
> >
<Box sx={{ <Box
sx={{
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
alignItems: "start", alignItems: "start",
maxWidth: "500px", maxWidth: "500px",
width: upMd ? "43.1%" : undefined, width: upMd ? "43.1%" : undefined,
mb: "10px", mb: "10px",
}}> }}
>
<Typography <Typography
variant="h4" variant="h4"
sx={{ sx={{
@ -63,7 +65,11 @@ export default function Section3() {
<UnderlinedLink <UnderlinedLink
linkHref="#" linkHref="#"
text="Подробнее" text="Подробнее"
endIcon={<ArrowForwardIcon sx={{ height: "20px", width: "20px", display: "inline" }} />} endIcon={
<ArrowForwardIcon
sx={{ height: "20px", width: "20px", display: "inline" }}
/>
}
/> />
</Box> </Box>
<PromoCard <PromoCard
@ -82,7 +88,7 @@ export default function Section3() {
textOrientation="row" textOrientation="row"
small={downXs} small={downXs}
backgroundImage={downXs ? cardPagesBackground5 : cardPagesBackground2} backgroundImage={downXs ? cardPagesBackground5 : cardPagesBackground2}
sx={{ alignSelf: "center" }} sx={{ alignSelf: "center", mt: upMd ? "-82px" : null }}
/> />
<PromoCard <PromoCard
width={upMd ? "43.1%" : "100%"} width={upMd ? "43.1%" : "100%"}

@ -1,14 +1,22 @@
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[];
@ -17,32 +25,44 @@ interface Props {
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)
.then((result) => {
devlog(result); devlog(result);
enqueueSnackbar("Тариф создан"); enqueueSnackbar("Тариф создан");
}).catch(error => { })
const message = getMessageFromFetchError(error, "Не удалось создать тариф"); .catch((error) => {
const message = getMessageFromFetchError(
error,
"Не удалось создать тариф"
);
if (message) enqueueSnackbar(message); if (message) enqueueSnackbar(message);
}); });
} }
return ( return (
<Box sx={{ <Box
sx={{
backgroundColor: "white", backgroundColor: "white",
width: "100%", width: "100%",
display: "flex", display: "flex",
flexDirection: upMd ? "row" : "column", flexDirection: upMd ? "row" : "column",
borderRadius: "12px", borderRadius: "12px",
boxShadow: cardShadow, boxShadow: cardShadow,
}}> }}
<Box sx={{ >
<Box
sx={{
p: "20px", p: "20px",
pr: upMd ? "35px" : undefined, pr: upMd ? "35px" : undefined,
display: "flex", display: "flex",
@ -51,16 +71,19 @@ export default function CustomTariffCard({ serviceKey, privileges }: Props) {
flexWrap: "wrap", flexWrap: "wrap",
flexDirection: "column", flexDirection: "column",
gap: "25px", gap: "25px",
}}> }}
{privileges.map(privilege => >
<TariffPrivilegeSlider {privileges.map((privilege) => (
key={privilege._id} <TariffPrivilegeSlider key={privilege._id} privilege={privilege} />
privilege={privilege} ))}
</Box>
{!upMd && (
<Divider
sx={{ mx: "20px", my: "10px", borderColor: theme.palette.grey2.main }}
/> />
)} )}
</Box> <Box
{!upMd && <Divider sx={{ mx: "20px", my: "10px", borderColor: theme.palette.grey2.main }} />} sx={{
<Box sx={{
display: "flex", display: "flex",
flexBasis: 0, flexBasis: 0,
flexGrow: 1, flexGrow: 1,
@ -70,40 +93,41 @@ export default function CustomTariffCard({ serviceKey, privileges }: Props) {
color: theme.palette.grey3.main, color: theme.palette.grey3.main,
p: "20px", p: "20px",
pl: upMd ? "33px" : undefined, pl: upMd ? "33px" : undefined,
borderLeft: upMd ? `1px solid ${theme.palette.grey2.main}` : undefined, borderLeft: upMd
}}> ? `1px solid ${theme.palette.grey2.main}`
<Box sx={{ : undefined,
}}
>
<Box
sx={{
display: "flex", display: "flex",
justifyContent: "space-between", justifyContent: "space-between",
gap: "15%", gap: "15%",
mb: "auto", mb: "auto",
width: "100%", width: "100%",
}}> }}
<Typography>Чем больше пакеты, тем дешевле подписки и опции </Typography> >
<Box sx={{ <Typography>
px: "6.7px", Чем больше пакеты, тем дешевле подписки и опции{" "}
height: "36px", </Typography>
color: "white",
backgroundColor: theme.palette.orange.main,
display: "flex",
justifyContent: "center",
alignItems: "center",
borderRadius: "8px",
}}>
{"-60%"}
</Box>
</Box> </Box>
<Typography mb="20px" mt="18px"> <Typography mb="20px" mt="18px">
Сумма с учетом скидки Сумма с учетом скидки
</Typography> </Typography>
<Box sx={{ <Box
sx={{
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
gap: "20px", gap: "20px",
mb: "30px", mb: "30px",
}}> }}
<Typography variant="price">{currencyFormatter.format(priceAfterDiscounts / 100)}</Typography> >
<Typography variant="oldPrice" pt="3px">{currencyFormatter.format(priceBeforeDiscounts / 100)}</Typography> <Typography variant="price">
{currencyFormatter.format(priceAfterDiscounts / 100)}
</Typography>
<Typography variant="oldPrice" pt="3px">
{currencyFormatter.format(priceBeforeDiscounts / 100)}
</Typography>
</Box> </Box>
<CustomButton <CustomButton
variant="contained" variant="contained"

@ -9,16 +9,27 @@ 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
@ -37,7 +48,8 @@ export default function TariffConstructor() {
gap: "80px", gap: "80px",
}} }}
> >
{Object.entries(customTariffs).map(([serviceKey, privileges], index) => ( {Object.entries(customTariffs).map(
([serviceKey, privileges], index) => (
<Box key={serviceKey}> <Box key={serviceKey}>
<Box <Box
sx={{ sx={{
@ -63,11 +75,14 @@ export default function TariffConstructor() {
text2={serviceNameByKey[serviceKey]} text2={serviceNameByKey[serviceKey]}
/> />
</Box> </Box>
<CustomTariffCard serviceKey={serviceKey} privileges={privileges} /> <CustomTariffCard
serviceKey={serviceKey}
privileges={privileges}
/>
</Box> </Box>
))} )
)}
</Box> </Box>
{upMd && (
<Link <Link
to="/tariffconstructor/savedtariffs" to="/tariffconstructor/savedtariffs"
style={{ style={{
@ -80,7 +95,6 @@ export default function TariffConstructor() {
> >
Ваши сохраненные тарифы Ваши сохраненные тарифы
</Link> </Link>
)}
<TotalPrice <TotalPrice
priceBeforeDiscounts={basePrice} priceBeforeDiscounts={basePrice}
priceAfterDiscounts={discountedPrice} priceAfterDiscounts={discountedPrice}

@ -1,31 +1,30 @@
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 {
@ -35,14 +34,21 @@ interface Props {
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 discounts = useDiscountStore((state) => state.discounts);
const currentCartTotal = useCartStore(
(state) => state.cart.priceAfterDiscounts
);
const purchasesAmount =
useUserStore((state) => state.userAccount?.wallet.purchasesAmount) ?? 0;
const [value, setValue] = useState<number>(userValue); const [value, setValue] = useState<number>(userValue);
const throttledValue = useThrottle(value, 200); const throttledValue = useThrottle(value, 200);
useEffect(function setStoreValue() { useEffect(
function setStoreValue() {
setCustomTariffsUserValue( setCustomTariffsUserValue(
privilege.serviceKey, privilege.serviceKey,
privilege._id, privilege._id,
@ -51,10 +57,20 @@ export default function TariffPrivilegeSlider({ privilege }: Props) {
currentCartTotal, currentCartTotal,
purchasesAmount 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);
} }
@ -62,60 +78,79 @@ export default function TariffPrivilegeSlider({ privilege }: Props) {
const quantityText = `${value} ${getDeclension(value, privilege.value)}`; const quantityText = `${value} ${getDeclension(value, privilege.value)}`;
const quantityElement = ( const quantityElement = (
<Box sx={{ <Box
sx={{
display: "flex", display: "flex",
gap: "15px", gap: "15px",
alignItems: "center", alignItems: "center",
justifyContent: upMd ? "end" : undefined, justifyContent: upMd ? "end" : undefined,
flexWrap: "wrap", flexWrap: "wrap",
mt: upMd ? undefined : "12px", mt: upMd ? undefined : "12px",
}}> }}
<Typography variant="p1" color={theme.palette.brightPurple.main} textAlign="end"> >
<Typography
variant="p1"
color={theme.palette.brightPurple.main}
textAlign="end"
>
{quantityText} {quantityText}
</Typography> </Typography>
<Box sx={{ <Box
sx={{
display: "flex", display: "flex",
gap: "15px", gap: "15px",
alignItems: "center", alignItems: "center",
flexWrap: "wrap", flexWrap: "wrap",
}}> }}
<Typography sx={{ fontSize: "16px", lineHeight: "19px", mt: "1px" }}>или</Typography> >
<Typography sx={{ fontSize: "16px", lineHeight: "19px", mt: "1px" }}>
или
</Typography>
<NumberInputWithUnitAdornment <NumberInputWithUnitAdornment
id={"privilege_input_" + privilege._id} id={"privilege_input_" + privilege._id}
adornmentText={getDeclension(0, privilege.value)} adornmentText={getDeclension(0, privilege.value)}
onChange={value => setValue(value)} onChange={(value) => setValue(value)}
/> />
</Box> </Box>
</Box> </Box>
); );
const icon = privilege.type === "day" const icon =
? <CalendarIcon color={theme.palette.orange.main} bgcolor="#FEDFD0" /> privilege.type === "day" ? (
: <PieChartIcon color={theme.palette.orange.main} bgcolor="#FEDFD0" />; <CalendarIcon color={theme.palette.orange.main} bgcolor="#FEDFD0" />
) : (
<PieChartIcon color={theme.palette.orange.main} bgcolor="#FEDFD0" />
);
return ( return (
<Box> <Box>
<Typography sx={{ color: theme.palette.grey3.main, mb: "auto" }}> <Typography sx={{ color: theme.palette.grey3.main, mb: "auto" }}>
{privilege.description} {privilege.description}
</Typography> </Typography>
<Box sx={{ <Box
sx={{
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
mt: "40px", mt: "40px",
}}> }}
<Box sx={{ >
<Box
sx={{
display: "flex", display: "flex",
// flexWrap: "wrap", // flexWrap: "wrap",
alignItems: "center", alignItems: "center",
mb: "8px", mb: "8px",
justifyContent: "space-between", justifyContent: "space-between",
gap: "10px", gap: "10px",
}}> }}
<Box sx={{ >
<Box
sx={{
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
gap: "22px", gap: "22px",
}}> }}
>
{icon} {icon}
<Typography variant="h5">{privilege.name}</Typography> <Typography variant="h5">{privilege.name}</Typography>
</Box> </Box>
@ -123,10 +158,9 @@ export default function TariffPrivilegeSlider({ privilege }: Props) {
</Box> </Box>
<CustomSlider <CustomSlider
value={value} value={value}
defaultValue={0}
min={0} min={0}
max={sliderSettingsByType[privilege.value].max || 100}
onChange={handleSliderChange} onChange={handleSliderChange}
{...sliderSettingsByType[privilege.value]}
/> />
{!upMd && quantityElement} {!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={{*/}

@ -27,12 +27,15 @@ export default function TariffPage() {
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 purchasesAmount =
useUserStore((state) => state.userAccount?.wallet.purchasesAmount) ?? 0;
const cart = useCartStore((state) => state.cart);
const unit: string = String(location.pathname).slice(9); const unit: string = String(location.pathname).slice(9);
const StepperText: Record<string, string> = { const StepperText: Record<string, string> = {
@ -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}