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

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,48 +24,35 @@ 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}
{image &&
<img
src={image}
alt=""
style={{
objectFit: "contain",
width: "100%",
height: "150%",
display: "block",
marginTop: "calc(-18px - 11%)",
pointerEvents: "none",
}}
>
<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" ?
<Typography variant="h5">{headerText}</Typography>
<Typography mt="20px" mb="29px">{text}</Typography>
<UnderlinedLink
linkHref={linkHref}
text="Подробнее"
@ -109,21 +63,6 @@ export default function CardWithLink({
mb: "15px",
}}
/>
:
<CustomButton
variant="contained"
sx={{
backgroundColor: "white",
color: theme.palette.primary.main,
mt: "auto",
"&:hover": {
backgroundColor: "#dddddd",
}
}}
>
Подробнее
</CustomButton>
}
</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,12 +1,10 @@
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"));
@ -20,40 +18,45 @@ export default function Section1() {
}}
sx={{
display: "flex",
pt: upMd ? "70px" : "20px",
pb: "70px",
justifyContent: "space-between",
pt: upMd ? "20px" : "20px",
pb: "0px",
flexDirection: upMd ? "row" : "column",
}}
>
<Box
sx={{
<Box sx={{
display: "flex",
flexDirection: "column",
flexBasis: upMd ? "310px" : undefined,
gap: "70px",
flexGrow: 1,
order: upMd ? 1 : 2,
mb: upMd ? undefined : "30px",
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",
}}
>
{upMd && <PenaLogo width={180} />}
<Typography variant="h2">Сервисы прокачки маркетинга</Typography>
Подробнее
</CustomButton>
</Box>
<Box
sx={{
<Box sx={{
ml: upMd ? "-100px" : undefined,
mb: "-40px",
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",
}}
>
maxHeight: upMd ? "550px" : "300px",
}}>
<video
autoPlay
loop
@ -61,42 +64,15 @@ export default function Section1() {
playsInline
poster={previewMain}
style={{
display: "block",
width: "100%",
height: "100%",
// transform: upMd ? undefined : "rotate(-90deg)",
}}
>
<source src={mainShapeVideo} type="video/webm" />
Your browser doesn't support HTML5 video tag.
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>
);
}

@ -1,18 +1,19 @@
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() {
interface Props {
templaterOnly?: boolean;
}
export default function Section2({ templaterOnly }: Props) {
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
@ -25,10 +26,6 @@ export default function Section2() {
mb: "-90px",
}}
sx={{
display: "flex",
flexDirection: "column",
alignItems: upMd ? undefined : "center",
gap: upMd ? "93px" : "40px",
pt: upMd ? "90px" : "50px",
pb: "20px",
}}
@ -58,10 +55,10 @@ export default function Section2() {
alignItems: "start",
flexGrow: 1,
flexBasis: "65.5%",
mt: "10px",
mt: upMd ? "10px" : "30px",
}}
>
<Typography>
<Typography maxWidth="560px">
Сервисы помогают предпринимателям, маркетологам и агентствам сделать интернет-маркетинг прозрачным и
эффективным. С нами не придется тратить рекламный бюджет впустую и терять клиентов на сайте.
</Typography>
@ -75,48 +72,102 @@ export default function Section2() {
/>
</Box>
</Box>
<Box
sx={{
{templaterOnly ?
!upMd ?
<Box sx={{
mt: upMd ? "93px" : "55px",
display: "flex",
flexDirection: upMd ? "row" : "column",
gap: upMd ? "3.5%" : "30px",
}}
>
flexWrap: "wrap",
justifyContent: "space-evenly",
columnGap: "40px",
rowGap: "50px",
}}>
<CardWithLink
shadowType="dark"
buttonType="link"
height="434px"
width={upMd ? "31%" : "100%"}
headerText="Шаблонизатор"
text="Текст- это текст, который имеет некоторые характеристики реального письменного текс"
isHighlighted
linkHref="#"
video={icon1}
poster={preview1}
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
shadowType="dark"
buttonType="link"
height="434px"
width={upMd ? "31%" : "100%"}
headerText="Опросник"
text="Текст- это текст, который имеет некоторые характеристики реального письменного текс"
linkHref="#"
video={icon2}
poster={preview2}
image={card2Image}
/>
<CardWithLink
shadowType="dark"
buttonType="link"
height="434px"
width={upMd ? "31%" : "100%"}
headerText="Сокращатель ссылок"
text="Текст- это текст, который имеет некоторые характеристики реального письменного текс"
linkHref="#"
video={icon3}
poster={preview3}
image={card3Image}
/>
</Box>
}
</SectionWrapper>
);
}

@ -26,7 +26,7 @@ export default function Section3() {
display: "flex",
pt: upMd ? "170px" : "155px",
pb: upMd ? "100px" : "70px",
width: "fit-content",
// width: "fit-content",
margin: "auto",
flexDirection: upMd ? "row" : "column",
flexWrap: "wrap",
@ -35,16 +35,14 @@ export default function Section3() {
justifyContent: "space-between",
}}
>
<Box
sx={{
<Box sx={{
display: "flex",
flexDirection: "column",
alignItems: "start",
maxWidth: "500px",
width: upMd ? "43.1%" : undefined,
mb: "10px",
}}
>
}}>
<Typography
variant="h4"
sx={{
@ -69,29 +67,31 @@ export default function Section3() {
/>
</Box>
<PromoCard
width={upMd ? "43.1%" : undefined}
width={upMd ? "43.1%" : "100%"}
headerText="Общий кабинет"
text="Текст-заполнитель — это текст, который имеет некоторые характеристики реального письменного"
textOrientation="column"
small={downXs}
backgroundImage={downXs ? cardPagesBackground4 : cardPagesBackground1}
sx={{ alignSelf: "center" }}
/>
<PromoCard
width={upMd ? "43.1%" : undefined}
width={upMd ? "43.1%" : "100%"}
headerText="Общий кабинет"
text="Текст-заполнитель — это текст, который имеет некоторые характеристики реального письменного"
textOrientation="row"
small={downXs}
backgroundImage={downXs ? cardPagesBackground5 : cardPagesBackground2}
sx={{ alignSelf: "center" }}
/>
<PromoCard
width={upMd ? "43.1%" : undefined}
width={upMd ? "43.1%" : "100%"}
headerText="Гибкие тарифы"
text="Текст-заполнитель — это текст, который имеет некоторые характеристики реального письменного"
textOrientation="column"
small={downXs}
backgroundImage={downXs ? cardPagesBackground6 : cardPagesBackground3}
sx={{ mt: upMd ? "102px" : undefined }}
sx={{ mt: upMd ? "82px" : undefined, alignSelf: "center" }}
/>
</SectionWrapper>
);

@ -20,7 +20,7 @@ export default function Section4() {
justifyContent:"space-between",
columnGap: upMd ? undefined :"20px",
flexWrap: "wrap",
rowGap: "80px",
rowGap: "65px",
pt: upMd ? "90px" : "70px",
pb: upMd ? "112px" : "76px",
}}

@ -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 { }