применение визуала главной страницы к рабочей логике

This commit is contained in:
krokodilka 2023-05-12 11:35:54 +03:00
parent 5a9c0723ee
commit b3aeebc8fb
16 changed files with 890 additions and 471 deletions

@ -3,12 +3,14 @@
<component name="ChangeListManager">
<list default="true" id="3a23e62a-21bd-4f51-a89e-05a6cca5b71f" name="Changes" comment="">
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/pages/Basket/Basket.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/Basket/Basket.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/pages/Basket/CustomWrapper.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/Basket/CustomWrapper.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/pages/Tariffs/TariffCardTimeAndVolume.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/Tariffs/TariffCardTimeAndVolume.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/pages/Tariffs/TariffsTime.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/Tariffs/TariffsTime.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/pages/Tariffs/TariffsVolume.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/Tariffs/TariffsVolume.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/stores/BasketStore.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/stores/BasketStore.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/components/CardWithLink.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/components/CardWithLink.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/pages/Landing/Infographics.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/Landing/Infographics.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/pages/Landing/Landing.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/Landing/Landing.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/pages/Landing/Section1.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/Landing/Section1.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/pages/Landing/Section2.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/Landing/Section2.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/pages/Landing/Section3.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/Landing/Section3.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/pages/Landing/Section4.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/Landing/Section4.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/pages/Landing/Section5.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/Landing/Section5.tsx" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

@ -1,53 +1,20 @@
import { Box, Typography, useTheme } from "@mui/material";
import { Box, SxProps, Theme, Typography, useTheme } from "@mui/material";
import UnderlinedLink from "./UnderlinedLink";
import ArrowForwardIcon from "@mui/icons-material/ArrowForward";
import CustomButton from "./CustomButton";
interface Props {
image?: string;
video?: string;
poster?: string;
headerText: string;
text: string;
linkHref: string;
isHighlighted?: boolean;
height: string;
width: string;
buttonType: "link" | "button";
shadowType: "dark" | "light";
sx?: SxProps<Theme>;
}
export default function CardWithLink({
image,
video,
poster,
headerText,
text,
linkHref,
isHighlighted = false,
height,
width,
buttonType,
shadowType
}: Props) {
export default function CardWithLink({ image, headerText, text, linkHref, isHighlighted = false, sx }: Props) {
const theme = useTheme();
const boxShadow: string = shadowType === "dark" ?
`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)`
:
`0px 100px 80px rgba(126, 42, 234, 0.07),
0px 41.7776px 33.4221px rgba(126, 42, 234, 0.0503198),
0px 22.3363px 17.869px rgba(126, 42, 234, 0.0417275),
0px 12.5216px 10.0172px rgba(126, 42, 234, 0.035),
0px 6.6501px 5.32008px rgba(126, 42, 234, 0.0282725),
0px 2.76726px 2.21381px rgba(126, 42, 234, 0.0196802)`;
return (
<Box
sx={{
@ -57,73 +24,45 @@ export default function CardWithLink({
alignItems: "start",
p: "20px",
maxWidth: "360px",
width,
height,
backgroundColor: isHighlighted ? theme.palette.brightPurple.main : theme.palette.grey1.main,
backgroundColor: isHighlighted ? "#7E2AEA" : theme.palette.grey1.main,
borderRadius: "12px",
color: "white",
boxShadow,
boxShadow: `
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)
`,
...sx,
}}
>
<Box sx={{
alignSelf: "center",
backgroundImage: image ? `url(${image})` : undefined,
width: "100%",
height: "100%",
maxHeight: "196px",
flexGrow: 1,
backgroundSize: "contain",
backgroundRepeat: "no-repeat",
backgroundPosition: "center",
display: "flex",
alignItems: "center",
overflow: "hidden",
}}>
{video &&
<video
autoPlay
loop
muted
playsInline
poster={poster}
style={{
width: "100%",
height: "150%",
}}
>
<source src={video} type="video/webm" />
Your browser doesn't support HTML5 video tag.
</video>
}
</Box>
<Typography variant="h5" sx={{ my: "12px" }}>{headerText}</Typography>
<Typography>{text}</Typography>
{buttonType === "link" ?
<UnderlinedLink
linkHref={linkHref}
text="Подробнее"
endIcon={<ArrowForwardIcon sx={{ height: "20px", width: "20px" }} />}
isHighlighted={isHighlighted}
sx={{
mt: "auto",
mb: "15px",
}}
/>
:
<CustomButton
variant="contained"
sx={{
backgroundColor: "white",
color: theme.palette.primary.main,
mt: "auto",
"&:hover": {
backgroundColor: "#dddddd",
}
}}
>
Подробнее
</CustomButton>
{image &&
<img
src={image}
alt=""
style={{
objectFit: "contain",
width: "100%",
display: "block",
marginTop: "calc(-18px - 11%)",
pointerEvents: "none",
}}
/>
}
</Box >
<Typography variant="h5">{headerText}</Typography>
<Typography mt="20px" mb="29px">{text}</Typography>
<UnderlinedLink
linkHref={linkHref}
text="Подробнее"
endIcon={<ArrowForwardIcon sx={{ height: "20px", width: "20px" }} />}
isHighlighted={isHighlighted}
sx={{
mt: "auto",
mb: "15px",
}}
/>
</Box>
);
}

@ -25,7 +25,7 @@ export default function Infographics({ bigText, text, flex }: Props) {
whiteSpace: "nowrap",
}}
>{bigText}</Typography>
<Typography variant={upMd ? "body1" : "body2"} sx={{ maxWidth: "10em", fontWeight: 500 }}>{text}</Typography>
<Typography variant={upMd ? "body1" : "body2"} sx={{ maxWidth: "11em", fontWeight: 500 }}>{text}</Typography>
</Box>
);
}

@ -6,15 +6,18 @@ import Section4 from "./Section4";
import Section5 from "./Section5";
import FloatingSupportChat from "@root/components/FloatingSupportChat/FloatingSupportChat";
interface Props {
templaterOnly?: boolean;
}
export default function Landing() {
export default function Landing({ templaterOnly = false }: Props) {
return (
<Box sx={{
position: "relative",
}}>
<Section1 />
<Section2 />
<Section2 templaterOnly={templaterOnly}/>
<Section3 />
<Section4 />
<Section5 />

@ -1,102 +1,78 @@
import { Box, Typography, useMediaQuery, useTheme } from "@mui/material";
import CustomButton from "@components/CustomButton";
import PenaLogo from "@components/PenaLogo";
import SectionWrapper from "@components/SectionWrapper";
import mainShapeVideo from "../../assets/animations/main.webm";
import previewMain from "../../assets/animations/preview_main.png";
export default function Section1() {
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
return (
<SectionWrapper
component="section"
maxWidth="lg"
outerContainerSx={{
backgroundColor: theme.palette.lightPurple.main,
}}
sx={{
display: "flex",
pt: upMd ? "70px" : "20px",
pb: "70px",
justifyContent: "space-between",
flexDirection: upMd ? "row" : "column",
}}
>
<Box
sx={{
display: "flex",
flexDirection: "column",
flexBasis: upMd ? "310px" : undefined,
gap: "70px",
order: upMd ? 1 : 2,
mb: upMd ? undefined : "30px",
}}
>
{upMd && <PenaLogo width={180} />}
<Typography variant="h2">Сервисы прокачки маркетинга</Typography>
</Box>
<Box
sx={{
flexShrink: 1,
textAlign: "center",
order: upMd ? 2 : 1,
mx: upMd ? "30px" : 0,
// mt: upMd ? undefined : "-70px",
// mb: upMd ? undefined : "-30px",
alignSelf: "center",
aspectRatio: "1 / 1",
width: upMd ? undefined : "100%",
maxWidth: "301px",
maxHeight: "301px",
}}
>
<video
autoPlay
loop
muted
playsInline
poster={previewMain}
style={{
width: "100%",
height: "100%",
// transform: upMd ? undefined : "rotate(-90deg)",
}}
export default function Section1() {
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
return (
<SectionWrapper
component="section"
maxWidth="lg"
outerContainerSx={{
backgroundColor: theme.palette.lightPurple.main,
}}
sx={{
display: "flex",
pt: upMd ? "20px" : "20px",
pb: "0px",
flexDirection: upMd ? "row" : "column",
}}
>
<source src={mainShapeVideo} type="video/webm" />
Your browser doesn't support HTML5 video tag.
</video>
</Box>
<Box
sx={{
display: "flex",
flexDirection: "column",
flexBasis: upMd ? "360px" : undefined,
alignItems: "start",
alignSelf: upMd ? "center" : "start",
mt: upMd ? "70px" : undefined,
order: 3,
}}
>
<Box sx={{ mb: "11px" }}>
<Typography>Покажут эффективность рекламы</Typography>
<Typography>Соберут все обращения клиентов</Typography>
<Typography>Повысят конверсию сайта</Typography>
</Box>
<Typography sx={{ mb: "40px" }}>И все это в едином кабинете</Typography>
<CustomButton
variant="contained"
sx={{
backgroundColor: theme.palette.brightPurple.main,
color: theme.palette.primary.main,
}}
>
Подробнее
</CustomButton>
</Box>
</SectionWrapper>
);
}
<Box sx={{
display: "flex",
flexDirection: "column",
flexGrow: 1,
order: upMd ? 1 : 2,
maxWidth: "700px",
mt: upMd ? "85px" : "40px",
mb: upMd ? "101px" : "70px",
}}>
<Typography variant="h2">Сервисы прокачки маркетинга</Typography>
<Typography mt="35px" maxWidth="560px">Покажут эффективность рекламы. Соберут все обращения клиентов. Повысят конверсию сайта</Typography>
<Typography mt="11.5px">И все это в едином кабинете</Typography>
<CustomButton
variant="contained"
sx={{
backgroundColor: theme.palette.brightPurple.main,
color: theme.palette.primary.main,
mt: "40px",
}}
>
Подробнее
</CustomButton>
</Box>
<Box sx={{
ml: upMd ? "-100px" : undefined,
mb: "-40px",
flexShrink: 1,
textAlign: "center",
order: upMd ? 2 : 1,
alignSelf: "center",
aspectRatio: "1 / 1",
width: upMd ? undefined : "100%",
maxHeight: upMd ? "550px" : "300px",
}}>
<video
autoPlay
loop
muted
playsInline
poster={previewMain}
style={{
display: "block",
width: "100%",
height: "100%",
}}
>
<source src={mainShapeVideo} type="video/webm" />
Your browser doesn"t support HTML5 video tag.,
</video>
</Box>
</SectionWrapper>
);
}

@ -1,122 +1,173 @@
import { Box, Typography, useMediaQuery, useTheme } 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 card2Image from "@root/assets/landing/card2.png";
import card3Image from "@root/assets/landing/card3.png";
import cardImageBig from "@root/assets/landing/card1big.png";
import icon1 from "../../assets/animations/Icon_1.webm";
import icon2 from "../../assets/animations/Icon_2.webm";
import icon3 from "../../assets/animations/Icon_3.webm";
import preview1 from "../../assets/animations/preview_1.png";
import preview2 from "../../assets/animations/preview_2.png";
import preview3 from "../../assets/animations/preview_3.png";
export default function Section2() {
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
return (
<SectionWrapper
component="section"
maxWidth="lg"
outerContainerSx={{
backgroundColor: theme.palette.darkPurple.main,
mb: "-90px",
}}
sx={{
display: "flex",
flexDirection: "column",
alignItems: upMd ? undefined : "center",
gap: upMd ? "93px" : "40px",
pt: upMd ? "90px" : "50px",
pb: "20px",
}}
>
<Box
sx={{
display: "flex",
flexDirection: upMd ? "row" : "column",
gap: "3.5%",
}}
>
<Typography
variant="h4"
sx={{
flexGrow: 1,
flexBasis: "31%",
maxWidth: "50%",
}}
>
Интеграции, избавляющие от рутины
</Typography>
<Box
sx={{
display: "flex",
flexDirection: "column",
gap: "30px",
alignItems: "start",
flexGrow: 1,
flexBasis: "65.5%",
mt: "10px",
}}
>
<Typography>
Сервисы помогают предпринимателям, маркетологам и агентствам сделать интернет-маркетинг прозрачным и
эффективным. С нами не придется тратить рекламный бюджет впустую и терять клиентов на сайте.
</Typography>
<UnderlinedLink
linkHref="#"
text="Подробнее"
endIcon={<ArrowForwardIcon sx={{ height: "20px", width: "20px" }} />}
sx={{
mt: "auto",
}}
/>
</Box>
</Box>
<Box
sx={{
display: "flex",
flexDirection: upMd ? "row" : "column",
gap: upMd ? "3.5%" : "30px",
}}
>
<CardWithLink
shadowType="dark"
buttonType="link"
height="434px"
width={upMd ? "31%" : "100%"}
headerText="Шаблонизатор"
text="Текст- это текст, который имеет некоторые характеристики реального письменного текс"
isHighlighted
linkHref="#"
video={icon1}
poster={preview1}
/>
<CardWithLink
shadowType="dark"
buttonType="link"
height="434px"
width={upMd ? "31%" : "100%"}
headerText="Опросник"
text="Текст- это текст, который имеет некоторые характеристики реального письменного текс"
linkHref="#"
video={icon2}
poster={preview2}
/>
<CardWithLink
shadowType="dark"
buttonType="link"
height="434px"
width={upMd ? "31%" : "100%"}
headerText="Сокращатель ссылок"
text="Текст- это текст, который имеет некоторые характеристики реального письменного текс"
linkHref="#"
video={icon3}
poster={preview3}
/>
</Box>
</SectionWrapper>
);
interface Props {
templaterOnly?: boolean;
}
export default function Section2({ templaterOnly }: Props) {
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
return (
<SectionWrapper
component="section"
maxWidth="lg"
outerContainerSx={{
backgroundColor: theme.palette.darkPurple.main,
mb: "-90px",
}}
sx={{
pt: upMd ? "90px" : "50px",
pb: "20px",
}}
>
<Box
sx={{
display: "flex",
flexDirection: upMd ? "row" : "column",
gap: "3.5%",
}}
>
<Typography
variant="h4"
sx={{
flexGrow: 1,
flexBasis: "31%",
maxWidth: "50%",
}}
>
Интеграции, избавляющие от рутины
</Typography>
<Box
sx={{
display: "flex",
flexDirection: "column",
gap: "30px",
alignItems: "start",
flexGrow: 1,
flexBasis: "65.5%",
mt: upMd ? "10px" : "30px",
}}
>
<Typography maxWidth="560px">
Сервисы помогают предпринимателям, маркетологам и агентствам сделать интернет-маркетинг прозрачным и
эффективным. С нами не придется тратить рекламный бюджет впустую и терять клиентов на сайте.
</Typography>
<UnderlinedLink
linkHref="#"
text="Подробнее"
endIcon={<ArrowForwardIcon sx={{ height: "20px", width: "20px" }} />}
sx={{
mt: "auto",
}}
/>
</Box>
</Box>
{templaterOnly ?
!upMd ?
<Box sx={{
mt: upMd ? "93px" : "55px",
display: "flex",
flexWrap: "wrap",
justifyContent: "space-evenly",
columnGap: "40px",
rowGap: "50px",
}}>
<CardWithLink
headerText="Шаблонизатор"
text="Текст- это текст, который имеет некоторые характеристики реального письменного текс"
linkHref="#"
image={card1Image}
isHighlighted={!upMd}
/>
</Box>
:
<Box sx={{
position: "relative",
mt: upMd ? "93px" : "55px",
// height: "244px",
display: "flex",
justifyContent: "space-between",
py: "40px",
px: "20px",
backgroundColor: "#434657",
borderRadius: "12px",
boxShadow: `
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)
`,
}}>
<Box sx={{
display: "flex",
flexDirection: "column",
}}>
<Typography variant="h5">Шаблонизатор</Typography>
<Typography mt="20px" maxWidth="552px">Текст- это текст, который имеет некоторые характеристики реального письменного текс</Typography>
<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 sx={{
mt: upMd ? "93px" : "55px",
display: "flex",
flexWrap: "wrap",
justifyContent: "space-evenly",
columnGap: "40px",
rowGap: "50px",
}}>
<CardWithLink
headerText="Шаблонизатор"
text="Текст- это текст, который имеет некоторые характеристики реального письменного текс"
linkHref="#"
image={card1Image}
isHighlighted={!upMd}
/>
<CardWithLink
headerText="Опросник"
text="Текст- это текст, который имеет некоторые характеристики реального письменного текс"
linkHref="#"
image={card2Image}
/>
<CardWithLink
headerText="Сокращатель ссылок"
text="Текст- это текст, который имеет некоторые характеристики реального письменного текс"
linkHref="#"
image={card3Image}
/>
</Box>
}
</SectionWrapper>
);
}

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

@ -9,28 +9,28 @@ export default function Section4() {
const itemsFlex = upMd ? "1 0 33.333%" : "1 0 50%";
return (
<SectionWrapper
component="section"
maxWidth="lg"
outerContainerSx={{
backgroundColor: theme.palette.darkPurple.main,
}}
sx={{
display: "flex",
justifyContent:"space-between",
columnGap: upMd ? undefined :"20px",
flexWrap: "wrap",
rowGap: "80px",
pt: upMd ? "90px" : "70px",
pb: upMd ? "112px" : "76px",
}}
>
<Infographics flex={itemsFlex} bigText="9" text="лет на рынке" />
<Infographics flex={itemsFlex} bigText="18" text="инструментов в едином кабинете" />
<Infographics flex={itemsFlex} bigText="5 467" text="клиентов с нами" />
<Infographics flex={itemsFlex} bigText="15" text="минут на подключение" />
<Infographics flex={itemsFlex} bigText="24/7" text="с вами служба поддержка" />
<Infographics flex={itemsFlex} bigText="1 000" text="рублей в месяц минимальный тариф" />
</SectionWrapper>
<SectionWrapper
component="section"
maxWidth="lg"
outerContainerSx={{
backgroundColor: theme.palette.darkPurple.main,
}}
sx={{
display: "flex",
justifyContent:"space-between",
columnGap: upMd ? undefined :"20px",
flexWrap: "wrap",
rowGap: "65px",
pt: upMd ? "90px" : "70px",
pb: upMd ? "112px" : "76px",
}}
>
<Infographics flex={itemsFlex} bigText="9" text="лет на рынке" />
<Infographics flex={itemsFlex} bigText="18" text="инструментов в едином кабинете" />
<Infographics flex={itemsFlex} bigText="5 467" text="клиентов с нами" />
<Infographics flex={itemsFlex} bigText="15" text="минут на подключение" />
<Infographics flex={itemsFlex} bigText="24/7" text="с вами служба поддержка" />
<Infographics flex={itemsFlex} bigText="1 000" text="рублей в месяц минимальный тариф" />
</SectionWrapper>
);
}
}

@ -4,53 +4,53 @@ import CustomButton from "@components/CustomButton";
import SectionWrapper from "@components/SectionWrapper";
export default function Section5() {
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
return (
<SectionWrapper
component="section"
maxWidth="lg"
outerContainerSx={{
backgroundColor: theme.palette.brightPurple.main,
}}
sx={{
pt: upMd ? "100px" : "80px",
pb: upMd ? "100px" : "80px",
}}
>
<Box
sx={{
display: "grid",
gridTemplate: upMd ? "auto auto / 1fr 1fr" : "repeat(3, auto) / auto",
}}
>
<Typography variant="h4" sx={{ mb: upMd ? "62px" : "30px" }}>
Остались вопросы?
</Typography>
<Typography
sx={{
maxWidth: "79.3%",
gridRow: upMd ? "span 2" : "",
justifySelf: upMd ? "end" : "start",
mb: upMd ? undefined : "50px",
}}
return (
<SectionWrapper
component="section"
maxWidth="lg"
outerContainerSx={{
backgroundColor: theme.palette.brightPurple.main,
}}
sx={{
pt: upMd ? "100px" : "80px",
pb: upMd ? "100px" : "80px",
}}
>
Сервисы помогают предпринимателям, маркетологам и агентствам сделать интернет-маркетинг прозрачным и
эффективным. С нами не придется тратить рекламный бюджет впустую и терять клиентов на сайте.
</Typography>
<Box
sx={{
display: "flex",
flexDirection: upMd ? "row" : "column",
alignItems: "start",
gap: upMd ? "24px" : "20px",
}}
>
<CustomButton variant="outlined">Подробнее</CustomButton>
<CustomButton variant="contained">Подробнее</CustomButton>
</Box>
</Box>
</SectionWrapper>
);
}
<Box
sx={{
display: "grid",
gridTemplate: upMd ? "auto auto / 1fr 1fr" : "repeat(3, auto) / auto",
}}
>
<Typography variant="h4" sx={{ mb: upMd ? "62px" : "30px" }}>
Остались вопросы?
</Typography>
<Typography
sx={{
maxWidth: "79.3%",
gridRow: upMd ? "span 2" : "",
justifySelf: upMd ? "end" : "start",
mb: upMd ? undefined : "50px",
}}
>
Сервисы помогают предпринимателям, маркетологам и агентствам сделать интернет-маркетинг прозрачным и
эффективным. С нами не придется тратить рекламный бюджет впустую и терять клиентов на сайте.
</Typography>
<Box
sx={{
display: "flex",
flexDirection: upMd ? "row" : "column",
alignItems: "start",
gap: upMd ? "24px" : "20px",
}}
>
<CustomButton variant="outlined">Подробнее</CustomButton>
<CustomButton variant="contained">Подробнее</CustomButton>
</Box>
</Box>
</SectionWrapper>
);
}

@ -0,0 +1,106 @@
import { apiRequestHandler } from "./apiRequestHandler";
import { AuthenticationSuccessResponse } from "./types";
import "isomorphic-fetch";
describe("authentication", () => {
const authSuccessResultMatcher: AuthenticationSuccessResponse = {
id: expect.any(String),
email: expect.any(String),
login: expect.any(String),
phoneNumber: expect.any(String),
avatar: expect.any(String),
role: expect.any(String),
accessToken: expect.any(String),
};
describe("register route", () => {
it("should return an error if any field is empty", async () => {
const randomNumber = Math.floor(Math.random() * 1e6);
await expect(apiRequestHandler.register({
email: `test${randomNumber}@test.com`,
login: `test${randomNumber}`,
password: "password",
phoneNumber: ""
})).resolves.toBeInstanceOf(Error);
});
it("should return an error if user already exists", async () => {
const randomNumber = Math.floor(Math.random() * 1e6);
await apiRequestHandler.register({
email: `test${randomNumber}@test.com`,
login: `test${randomNumber}`,
password: "password",
phoneNumber: "1234"
});
await expect(apiRequestHandler.register({
email: `test${randomNumber}@test.com`,
login: `test${randomNumber}`,
password: "password",
phoneNumber: "12345"
})).resolves.toBeInstanceOf(Error);
});
it("should return user object on register success", async () => {
const randomNumber = Math.floor(Math.random() * 1e6);
const result = await apiRequestHandler.register({
email: `test${randomNumber}@test.com`,
login: `test${randomNumber}`,
password: "password",
phoneNumber: "1234567890"
});
expect(result).toStrictEqual(authSuccessResultMatcher);
});
});
describe("login route", () => {
const randomNumber = Math.floor(Math.random() * 1e6);
beforeAll(async () => {
await apiRequestHandler.register({
email: `test${randomNumber}@test.com`,
login: `test${randomNumber}`,
password: "password",
phoneNumber: "123456"
});
});
it("should return an error if any field is empty", async () => {
await expect(apiRequestHandler.login({
email: `test${randomNumber}@test.com`,
password: ""
})).resolves.toBeInstanceOf(Error);
});
it("should return an error if password is wrong", async () => {
await expect(apiRequestHandler.login({
email: `test${randomNumber}@test.com`,
password: "wrong_password"
})).resolves.toBeInstanceOf(Error);
});
it("should return user object on login success", async () => {
const result = await apiRequestHandler.login({
email: `test${randomNumber}@test.com`,
password: "password"
});
expect(result).toStrictEqual(authSuccessResultMatcher);
});
});
/**
* TODO
* 1. jest не отсылает куки с запросом -> возвращается пустой объект без refreshToken
* https://github.com/facebook/jest/issues/3547
* 2. Сервер возвращает https-only куки, но сервер использует http.
*/
describe("logout route", () => {
const randomNumber = Math.floor(Math.random() * 1e6);
it("should return refresh token", async () => {
await apiRequestHandler.register({
email: `test${randomNumber}@test.com`,
login: `test${randomNumber}`,
password: "password",
phoneNumber: "123456"
});
await apiRequestHandler.login({
email: `test${randomNumber}@test.com`,
password: "password",
});
const result = await apiRequestHandler.logout();
expect(result).toStrictEqual({ refreshToken: expect.any(String) });
});
});
});

@ -0,0 +1,236 @@
import {
RegistrationRequest,
AuthenticationSuccessResponse,
LoginRequest,
RefreshRequest,
CreateTicketRequest,
CreateTicketResponse,
SendTicketMessageRequest,
GetTicketsRequest,
GetTicketsResponse,
ApiError,
GetMessagesRequest,
GetMessagesResponse
} from "./types";
import ReconnectingEventSource from "reconnecting-eventsource";
const TEST_ACCESS_TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJJRCI6ImMxcDM0YjhqZHBmOG04Zm43cnIwIiwiU2Vzc2lvbiI6ImMxcDM0YjhqZHBmOG04Zm43cnJnIiwiVXNlciI6IiIsIlRhcmlmZiI6MCwiQ3JlYXRlZCI6MTYxODA5NjY4NTc4MCwiTGFzdFNlZW4iOjE2MTgwOTY2ODU3ODF9.ciJoJiOxzIPv0LY4h3rG8Tf3AsSBXXLcYEpyN9mIki0";
/** @deprecated */
class ApiRequestHandler {
private authApiUrl: string;
private supportApiUrl: string;
private accessToken?: string;
constructor({ authApiUrl, supportApiUrl }: {
authApiUrl: string;
supportApiUrl: string;
}) {
this.authApiUrl = authApiUrl;
this.supportApiUrl = supportApiUrl;
this.accessToken = TEST_ACCESS_TOKEN;
}
public async register(request: RegistrationRequest): Promise<AuthenticationSuccessResponse | ApiError | Error> {
try {
const response = await fetch(this.authApiUrl + "auth/register", {
method: "POST",
body: JSON.stringify(request),
headers: {
"Accept": "application/json, text/plain, */*",
"Content-Type": "application/json"
},
});
const result = await response.json();
if (result.error) return new ApiError(result.message);
this.accessToken = (result as AuthenticationSuccessResponse).accessToken;
return result as AuthenticationSuccessResponse;
} catch (error) {
return error as Error;
}
}
public async login(request: LoginRequest): Promise<AuthenticationSuccessResponse | ApiError | Error> {
try {
const response = await fetch(this.authApiUrl + "auth/login", {
method: "POST",
body: JSON.stringify(request),
headers: {
"Accept": "application/json, text/plain, */*",
"Content-Type": "application/json"
},
});
const result = await response.json();
if (result.error) return new ApiError(result.message);
this.accessToken = (result as AuthenticationSuccessResponse).accessToken;
return result as AuthenticationSuccessResponse;
} catch (error) {
return error as Error;
}
}
public async refresh(request: RefreshRequest) {
try {
const response = await fetch(this.authApiUrl + "auth/refresh", {
method: "POST",
credentials: "include",
body: JSON.stringify(request),
headers: {
"Accept": "application/json, text/plain, */*",
"Content-Type": "application/json"
},
});
const result = await response.json();
return result;
} catch (error) {
return error;
}
}
public async logout(): Promise<null | ApiError | Error> {
try {
const response = await fetch(this.authApiUrl + "auth/logout", {
method: "POST",
credentials: "include"
});
if (response.status !== 200) return new ApiError(`Unexpected status code. Expected: 200, received: ${response.status}`); // TODO correct success status code
return null;
} catch (error) {
return error as Error;
}
}
public async createTicket(request: CreateTicketRequest): Promise<CreateTicketResponse | ApiError | Error> {
try {
const response = await fetch(this.supportApiUrl + "support/create", {
method: "POST",
credentials: "include",
body: JSON.stringify(request),
headers: {
"Accept": "application/json, text/plain, */*",
"Content-Type": "application/json",
"Authorization": `Bearer ${this.accessToken}`,
},
});
const result = await response.json();
if (result.error) return new ApiError(result.message);
return result as CreateTicketResponse;
} catch (error) {
return error as Error;
}
}
public subscribeToAllTickets({ onMessage, onError }: {
onMessage: (e: MessageEvent) => void;
onError: (e: Event) => void;
}) {
if (!this.accessToken) throw new Error("Trying to subscribe to SSE without access token");
const url = `${this.supportApiUrl}/support/subscribe?Authorization=${this.accessToken}`;
const eventSource = new ReconnectingEventSource(url);
eventSource.addEventListener("open", () => console.log(`EventSource connected with ${url}`));
eventSource.addEventListener("close", () => console.log(`EventSource closed with ${url}`));
eventSource.addEventListener("message", onMessage);
eventSource.addEventListener("error", onError);
/**
* TODO
* Похоже, что сервер закрывает соединение после каждого сообщения и EventSource выдает ошибку, после чего переподключается и так каждые 3 секунды
* https://stackoverflow.com/questions/66047672/server-sent-events-error-despite-http-200
* https://stackoverflow.com/questions/53888030/why-are-server-sent-events-fired-every-3-seconds
*/
return () => {
eventSource.close();
};
}
public subscribeToTicket({ ticketId, onMessage, onError }: {
ticketId: string;
onMessage: (e: MessageEvent) => void;
onError: (e: Event) => void;
}) {
if (!this.accessToken) throw new Error("Trying to subscribe to SSE without access token");
const url = `${this.supportApiUrl}/support/ticket?ticket=${ticketId}&Authorization=${this.accessToken}`;
const eventSource = new ReconnectingEventSource(url);
eventSource.addEventListener("open", () => console.log(`EventSource connected with ${url}`));
eventSource.addEventListener("close", () => console.log(`EventSource closed with ${url}`));
eventSource.addEventListener("message", onMessage);
eventSource.addEventListener("error", onError);
return () => {
eventSource.close();
};
}
public async sendTicketMessage(request: SendTicketMessageRequest): Promise<null | ApiError | Error> {
try {
const response = await fetch(this.supportApiUrl + "support/send", {
method: "POST",
credentials: "include",
body: JSON.stringify(request),
headers: {
"Accept": "application/json, text/plain, */*",
"Content-Type": "application/json",
"Authorization": `Bearer ${this.accessToken}`,
},
});
if (response.status !== 200) return new ApiError(`Unexpected status code. Expected: 200, received: ${response.status}`); // TODO correct success status code
return null;
} catch (error) {
return error as Error;
}
}
public async getTickets(request: GetTicketsRequest, signal: AbortSignal): Promise<GetTicketsResponse | ApiError | Error> {
try {
const response = await fetch(this.supportApiUrl + "support/getTickets", {
method: "POST",
credentials: "include",
body: JSON.stringify(request),
headers: {
"Accept": "application/json, text/plain, */*",
"Content-Type": "application/json",
"Authorization": `Bearer ${this.accessToken}`,
},
signal,
});
const result = await response.json();
if (result.error) return new ApiError(result.message);
return result as GetTicketsResponse;
} catch (error) {
return error as Error;
}
}
public async getMessages(request: GetMessagesRequest, signal: AbortSignal): Promise<GetMessagesResponse | ApiError | Error> {
try {
const response = await fetch(this.supportApiUrl + "support/getMessages", {
method: "POST",
credentials: "include",
body: JSON.stringify(request),
headers: {
"Accept": "application/json, text/plain, */*",
"Content-Type": "application/json",
"Authorization": `Bearer ${this.accessToken}`,
},
signal,
});
const result = await response.json();
if (result.error) return new ApiError(result.message);
return result as GetMessagesResponse;
} catch (error) {
return error as Error;
}
}
}
/** @deprecated */
export const apiRequestHandler = new ApiRequestHandler({
authApiUrl: "http://localhost:8080/",
supportApiUrl: "http://localhost:1488/",
});

106
src/utils/api/types.ts Normal file

@ -0,0 +1,106 @@
/** @deprecated */
export type LoginRequest = {
email?: string;
password?: string;
};
/** @deprecated */
export type RegistrationRequest = {
login?: string;
email?: string;
phoneNumber?: string;
password?: string;
};
/** @deprecated */
export type AuthenticationSuccessResponse = {
id: string;
email: string;
login: string;
phoneNumber: string;
avatar: string;
role: string;
accessToken: string;
};
/** @deprecated */
export type RefreshRequest = {
userId: string;
refreshToken?: string;
};
/** @deprecated */
export type CreateTicketRequest = {
Title: string;
Message: string;
};
/** @deprecated */
export type CreateTicketResponse = {
Ticket: string;
};
/** @deprecated */
export type SendTicketMessageRequest = {
message: string;
ticket: string;
lang: string;
files: string[];
};
/** @deprecated */
export type GetTicketsRequest = {
amt: number;
page: number;
srch: string;
status: string;
};
/** @deprecated */
export type GetTicketsResponse = {
count: number;
data: Ticket[];
};
/** @deprecated */
export type Ticket = {
id: string;
user: string;
sess: string;
ans: string;
state: string;
top_message: TicketMessage;
title: string;
created_at: string;
updated_at: string;
rate: number;
};
/** @deprecated */
export type TicketMessage = {
id: string;
ticket_id: string;
user_id: string,
session_id: string;
message: string;
files: string[],
shown: { [key: string]: number; },
request_screenshot: string,
created_at: string;
};
/** @deprecated */
export type GetMessagesRequest = {
amt: number;
page: number;
srch: string;
ticket: string;
};
/** @deprecated */
export type GetMessagesResponse = TicketMessage[];
/** @deprecated */
export class ApiError extends Error { }