применение визуала главной страницы к рабочей логике
This commit is contained in:
parent
5a9c0723ee
commit
b3aeebc8fb
@ -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" />
|
||||
|
BIN
src/assets/landing/card1.png
Normal file
BIN
src/assets/landing/card1.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 111 KiB |
BIN
src/assets/landing/card1big.png
Normal file
BIN
src/assets/landing/card1big.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 197 KiB |
BIN
src/assets/landing/card2.png
Normal file
BIN
src/assets/landing/card2.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 112 KiB |
BIN
src/assets/landing/card3.png
Normal file
BIN
src/assets/landing/card3.png
Normal file
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>
|
||||
);
|
||||
}
|
106
src/utils/api/apiRequestHandler.test.ts
Normal file
106
src/utils/api/apiRequestHandler.test.ts
Normal file
@ -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) });
|
||||
});
|
||||
});
|
||||
});
|
236
src/utils/api/apiRequestHandler.ts
Normal file
236
src/utils/api/apiRequestHandler.ts
Normal file
@ -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
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 { }
|
Loading…
Reference in New Issue
Block a user