аbsolute import(components, utils), remove App.tsx

This commit is contained in:
ArtChaos189 2023-03-19 15:30:40 +03:00
parent 07677825c6
commit e681c52bce
28 changed files with 2068 additions and 2028 deletions

@ -1,132 +0,0 @@
import { CssBaseline, Divider, ThemeProvider, useMediaQuery } from "@mui/material";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import Landing from "./pages/Landing/Landing";
import Signin from "./pages/Signin";
import Signup from "./pages/Signup";
import Tariffs from "./pages/Tariffs/Tariffs";
import { TariffsTime } from "./pages/Tariffs/TariffsTime";
import Faq from "./pages/Faq/Faq";
import Wallet from "./pages/Wallet";
import Payment from "./pages/Payment/Payment";
import Support from "./pages/Support/Support";
import CustomTariff from "./pages/CustomTariff/CustomTariff";
import { TariffsVolume } from "./pages/Tariffs/TariffsVolume";
import { AccountSetup } from "./pages/AccountSetup";
import Footer from "./components/Footer";
import Navbar from "./components/Navbar/Navbar";
import darkTheme from "./utils/themes/dark";
import lightTheme from "./utils/themes/light";
import { SnackbarProvider } from "notistack";
function App() {
const upMd = useMediaQuery(lightTheme.breakpoints.up("md"));
return (
<SnackbarProvider>
<ThemeProvider theme={lightTheme}>
<CssBaseline />
<BrowserRouter>
<Routes>
<Route
path="/"
element={
<ThemeProvider theme={darkTheme}>
<CssBaseline />
<Navbar isLoggedIn={false} isCollapsed={!upMd} />
<Divider sx={{ bgcolor: "#E3E3E3", borderColor: "#00000000" }} />
<Landing />
<Footer />
</ThemeProvider>
}
/>
<Route path="/signin" element={<Signin />} />
<Route path="/signup" element={<Signup />} />
<Route
path="tariffs/*"
element={
<>
<Navbar isLoggedIn={true} isCollapsed={!upMd} />
<Tariffs />
</>
}
>
<Route path="time" element={<TariffsTime />} />
<Route path="volume" element={<TariffsVolume />} />
</Route>
<Route
path="/faq"
element={
<>
<Navbar isLoggedIn={true} isCollapsed={!upMd} />
<Faq />
</>
}
/>
<Route
path="/wallet"
element={
<>
<Navbar isLoggedIn={true} isCollapsed={!upMd} />
<Wallet />
</>
}
/>
<Route
path="/payment"
element={
<>
<Navbar isLoggedIn={true} isCollapsed={!upMd} />
<Payment />
</>
}
/>
<Route
path="/support"
element={
<>
<Navbar isLoggedIn={true} isCollapsed={!upMd} />
<Support />
</>
}
/>
<Route
path="/support/:ticketId"
element={
<>
<Navbar isLoggedIn={true} isCollapsed={!upMd} />
<Support />
</>
}
/>
<Route
path="/tariffconstructor"
element={
<>
<Navbar isLoggedIn={true} isCollapsed={!upMd} />
<CustomTariff />
</>
}
/>
<Route
path="/settings"
element={
<>
<Navbar isLoggedIn={true} isCollapsed={!upMd} />
<AccountSetup />
</>
}
/>
</Routes>
</BrowserRouter>
</ThemeProvider>
</SnackbarProvider>
);
}
export default App;

@ -1,16 +1,143 @@
import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import { CssBaseline, Divider, ThemeProvider, useMediaQuery } from "@mui/material";
const root = ReactDOM.createRoot(
document.getElementById("root") as HTMLElement
);
import Faq from "./pages/Faq/Faq";
import Wallet from "./pages/Wallet";
import Payment from "./pages/Payment/Payment";
import Support from "./pages/Support/Support";
import CustomTariff from "./pages/CustomTariff/CustomTariff";
import { TariffsVolume } from "./pages/Tariffs/TariffsVolume";
import { AccountSetup } from "./pages/AccountSetup";
import Landing from "./pages/Landing/Landing";
import Tariffs from "./pages/Tariffs/Tariffs";
import { TariffsTime } from "./pages/Tariffs/TariffsTime";
import Signin from "./pages/Signin";
import Signup from "./pages/Signup";
import Footer from "@components/Footer";
import Navbar from "@components/Navbar/Navbar";
import darkTheme from "@utils/themes/dark";
import lightTheme from "@utils/themes/light";
import { SnackbarProvider } from "notistack";
export const App = () => {
const upMd = useMediaQuery(lightTheme.breakpoints.up("md"));
return (
<SnackbarProvider>
<ThemeProvider theme={lightTheme}>
<CssBaseline />
<BrowserRouter>
<Routes>
<Route
path="/"
element={
<ThemeProvider theme={darkTheme}>
<CssBaseline />
<Navbar isLoggedIn={false} isCollapsed={!upMd} />
<Divider sx={{ bgcolor: "#E3E3E3", borderColor: "#00000000" }} />
<Landing />
<Footer />
</ThemeProvider>
}
/>
<Route path="/signin" element={<Signin />} />
<Route path="/signup" element={<Signup />} />
<Route
path="tariffs/*"
element={
<>
<Navbar isLoggedIn={true} isCollapsed={!upMd} />
<Tariffs />
</>
}
>
<Route path="time" element={<TariffsTime />} />
<Route path="volume" element={<TariffsVolume />} />
</Route>
<Route
path="/faq"
element={
<>
<Navbar isLoggedIn={true} isCollapsed={!upMd} />
<Faq />
</>
}
/>
<Route
path="/wallet"
element={
<>
<Navbar isLoggedIn={true} isCollapsed={!upMd} />
<Wallet />
</>
}
/>
<Route
path="/payment"
element={
<>
<Navbar isLoggedIn={true} isCollapsed={!upMd} />
<Payment />
</>
}
/>
<Route
path="/support"
element={
<>
<Navbar isLoggedIn={true} isCollapsed={!upMd} />
<Support />
</>
}
/>
<Route
path="/support/:ticketId"
element={
<>
<Navbar isLoggedIn={true} isCollapsed={!upMd} />
<Support />
</>
}
/>
<Route
path="/tariffconstructor"
element={
<>
<Navbar isLoggedIn={true} isCollapsed={!upMd} />
<CustomTariff />
</>
}
/>
<Route
path="/settings"
element={
<>
<Navbar isLoggedIn={true} isCollapsed={!upMd} />
<AccountSetup />
</>
}
/>
</Routes>
</BrowserRouter>
</ThemeProvider>
</SnackbarProvider>
);
};
const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
<React.StrictMode>
<App />
</React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function

@ -1,7 +1,7 @@
import { Box, Button, Link, Typography, useMediaQuery, useTheme } from "@mui/material";
import CustomButton from "@root/components/CustomButton";
import InputTextfield from "@root/components/InputTextfield";
import SectionWrapper from "@root/components/SectionWrapper";
import CustomButton from "@components/CustomButton";
import InputTextfield from "@components/InputTextfield";
import SectionWrapper from "@components/SectionWrapper";
import Download from "../assets/Icons/Download.svg";
import Account from "../assets/Icons/Account.svg";

@ -1,34 +1,33 @@
import { Box } from "@mui/material";
import CustomAccordionBasket from "../../components/CustomAccordionBasket";
import CustomAccordionBasket from "@components/CustomAccordionBasket";
interface Props {
content:{title:string, data:[string,number][]}[]
content: { title: string; data: [string, number][] }[];
}
export default function AccordionWrapperBasket({content}:Props) {
return (
<Box
sx={{
overflow: "hidden",
borderRadius: "12px",
boxShadow: `0px 100px 309px rgba(210, 208, 225, 0.24),
export default function AccordionWrapperBasket({ content }: Props) {
return (
<Box
sx={{
overflow: "hidden",
borderRadius: "12px",
boxShadow: `0px 100px 309px rgba(210, 208, 225, 0.24),
0px 41.7776px 129.093px rgba(210, 208, 225, 0.172525),
0px 22.3363px 69.0192px rgba(210, 208, 225, 0.143066),
0px 12.5216px 38.6916px rgba(210, 208, 225, 0.12),
0px 6.6501px 20.5488px rgba(210, 208, 225, 0.0969343),
0px 2.76726px 8.55082px rgba(210, 208, 225, 0.0674749)`,
}}
>
{content.map((accordionItem, index) => (
<CustomAccordionBasket
key={index}
header={accordionItem.title}
dataSection={accordionItem.data}
totalPrice={3920}
/>
))}
</Box>
);
}}
>
{content.map((accordionItem, index) => (
<CustomAccordionBasket
key={index}
header={accordionItem.title}
dataSection={accordionItem.data}
totalPrice={3920}
/>
))}
</Box>
);
}

@ -1,112 +1,101 @@
import { Box, IconButton, Tabs, Typography, useMediaQuery, useTheme } from "@mui/material";
import ComplexNavText from "../../components/ComplexNavText";
import SectionWrapper from "../../components/SectionWrapper";
import { Box, IconButton, Typography, useMediaQuery, useTheme } from "@mui/material";
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import { useState } from "react";
import AccordionWrapperBasket from "./AccordionWrapper";
import TotalPrice from "../../components/TotalPrice";
import ComplexNavText from "@components/ComplexNavText";
import SectionWrapper from "@components/SectionWrapper";
import TotalPrice from "@components/TotalPrice";
import AccordionWrapperBasket from "./AccordionWrapper";
interface TabPanelProps {
index: number;
value: number;
children?: React.ReactNode;
mt: string;
index: number;
value: number;
children?: React.ReactNode;
mt: string;
}
function TabPanel({ index, value, children, mt }: TabPanelProps) {
return (
<Box
hidden={index !== value}
sx={{ mt }}
>
{children}
</Box>
);
return (
<Box hidden={index !== value} sx={{ mt }}>
{children}
</Box>
);
}
const contentBasket = [
{
title:"Шаблонизатор",
data:[
{
title: "Шаблонизатор",
data: [
["Дисковое хранилище 5 гб", 390],
["Подписка на месяц ", 290],
["200 бесплатных генераций", 590],
],
},
{
title: "Квиз конструктор",
data: [
["Дисковое хранилище 5 гб", 200],
["Подписка на месяц ", 300],
["200 бесплатных генераций", 1000],
],
},
];
export default function BasketPage() {
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const [tabIndex, setTabIndex] = useState<number>(0);
const handleChange = (event: React.SyntheticEvent, newValue: number) => {
setTabIndex(newValue);
};
return (
<SectionWrapper
maxWidth="lg"
sx={{
mt: upMd ? "25px" : "20px",
mb: upMd ? "70px" : "37px",
}}
>
{upMd && <ComplexNavText text1="Все тарифы — " text2="Корзина" />}
<Box
sx={{
mt: "20px",
mb: upMd ? "40px" : "20px",
display: "flex",
gap: "10px",
}}
>
{!upMd && (
<IconButton sx={{ p: 0, height: "28px", width: "28px", color: "black" }}>
<ArrowBackIcon />
</IconButton>
)}
<Typography variant="h4">Вопросы и ответы</Typography>
</Box>
<TabPanel value={tabIndex} index={0} mt={upMd ? "27px" : "10px"}>
<AccordionWrapperBasket
content={[
{
title: "Шаблонизатор",
data: [
["Дисковое хранилище 5 гб", 390],
["Подписка на месяц ", 290],
["200 бесплатных генераций", 590]
]
},
{
title:"Квиз конструктор",
data:[
["200 бесплатных генераций", 590],
],
},
{
title: "Квиз конструктор",
data: [
["Дисковое хранилище 5 гб", 200],
["Подписка на месяц ", 300],
["200 бесплатных генераций", 1000],
]
},
]
export default function BasketPage() {
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const [tabIndex, setTabIndex] = useState<number>(0);
const handleChange = (event: React.SyntheticEvent, newValue: number) => {
setTabIndex(newValue);
};
return (
<SectionWrapper
maxWidth="lg"
sx={{
mt: upMd ? "25px" : "20px",
mb: upMd ? "70px" : "37px",
}}
>
{upMd &&
<ComplexNavText text1="Все тарифы — " text2="Корзина" />
}
<Box
sx={{
mt: "20px",
mb: upMd ? "40px" : "20px",
display: "flex",
gap: "10px",
}}
>
{!upMd &&
<IconButton sx={{ p: 0, height: "28px", width: "28px", color: "black" }}>
<ArrowBackIcon />
</IconButton>
}
<Typography variant="h4">Вопросы и ответы</Typography>
</Box>
<TabPanel value={tabIndex} index={0} mt={upMd ? "27px" : "10px"}>
<AccordionWrapperBasket
content={[
{
title:"Шаблонизатор",
data:[
["Дисковое хранилище 5 гб", 390],
["Подписка на месяц ", 290],
["200 бесплатных генераций", 590]
]
},
{
title:"Квиз конструктор",
data:[
["Дисковое хранилище 5 гб", 200],
["Подписка на месяц ", 300],
["200 бесплатных генераций", 1000],
]
},
]}
/>
</TabPanel>
<TotalPrice/>
</SectionWrapper >
);
],
},
]}
/>
</TabPanel>
<TotalPrice />
</SectionWrapper>
);
}

@ -1,127 +1,129 @@
import { Box, IconButton, Typography, useMediaQuery, useTheme } from "@mui/material";
import CustomButton from "../../components/CustomButton";
import SectionWrapper from "../../components/SectionWrapper";
import TariffConstructorCard from "./TariffConstructorCard";
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import ComplexNavText from "../../components/ComplexNavText";
import ComplexHeader from "../../components/ComplexHeader";
import CustomButton from "@components/CustomButton";
import SectionWrapper from "@components/SectionWrapper";
import TariffConstructorCard from "./TariffConstructorCard";
import ComplexNavText from "@components/ComplexNavText";
import ComplexHeader from "@components/ComplexHeader";
export default function TariffConstructor() {
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
return (
<SectionWrapper
maxWidth="lg"
sx={{
mt: upMd ? "25px" : "20px",
mb: upMd ? "93px" : "48px",
}}
return (
<SectionWrapper
maxWidth="lg"
sx={{
mt: upMd ? "25px" : "20px",
mb: upMd ? "93px" : "48px",
}}
>
{upMd && <ComplexNavText text1="Все тарифы — " text2="Кастомный тариф" />}
<Box
sx={{
mt: "20px",
mb: "40px",
display: "flex",
gap: "10px",
}}
>
{!upMd && (
<IconButton sx={{ p: 0, height: "28px", width: "28px", color: "black" }}>
<ArrowBackIcon />
</IconButton>
)}
<ComplexHeader text1="Кастомный тариф " text2="Шаблонизатор" />
</Box>
<TariffConstructorCard
time="9 месяцев"
quantity="1000 шаблонов"
discountText="-60%"
totalText="3 190 руб."
totalWithoutDiscountText="10 190 руб."
/>
<ComplexHeader sx={{ mt: upMd ? "80px" : "70px", mb: "40px" }} text1="Кастомный тариф " text2="Опросник" />
<TariffConstructorCard
time="9 месяцев"
quantity="1000 шаблонов"
discountText="-60%"
totalText="3 190 руб."
totalWithoutDiscountText="10 190 руб."
/>
<Box
sx={{
display: "flex",
flexDirection: upMd ? "row" : "column",
mt: upMd ? "80px" : "70px",
pt: upMd ? "30px" : undefined,
borderTop: upMd ? `1px solid ${theme.palette.grey2.main}` : undefined,
}}
>
<Box
sx={{
width: upMd ? "68.5%" : undefined,
pr: upMd ? "15%" : undefined,
display: "flex",
flexWrap: "wrap",
flexDirection: "column",
}}
>
{upMd &&
<ComplexNavText text1="Все тарифы — " text2="Кастомный тариф" />
}
<Box
sx={{
mt: "20px",
mb: "40px",
display: "flex",
gap: "10px",
}}
<Typography variant="h4" mb={upMd ? "18px" : "30px"}>
Итоговая цена
</Typography>
<Typography color={theme.palette.grey3.main}>
Текст-заполнитель это текст, который имеет Текст-заполнитель это текст, который имеет Текст-заполнитель
это текст, который имеет Текст-заполнитель это текст, который имеет Текст-заполнитель
</Typography>
</Box>
<Box
sx={{
color: theme.palette.grey3.main,
width: upMd ? "31.5%" : undefined,
pl: upMd ? "33px" : undefined,
}}
>
<Box
sx={{
display: "flex",
flexDirection: upMd ? "column" : "row",
alignItems: upMd ? "start" : "center",
mt: upMd ? "10px" : "30px",
gap: "15px",
}}
>
<Typography
color={theme.palette.orange.main}
sx={{
textDecoration: "line-through",
order: upMd ? 1 : 2,
}}
>
{!upMd &&
<IconButton sx={{ p: 0, height: "28px", width: "28px", color: "black" }}>
<ArrowBackIcon />
</IconButton>
}
<ComplexHeader text1="Кастомный тариф " text2="Шаблонизатор" />
</Box>
<TariffConstructorCard
time="9 месяцев"
quantity="1000 шаблонов"
discountText="-60%"
totalText="3 190 руб."
totalWithoutDiscountText="10 190 руб."
/>
<ComplexHeader sx={{ mt: upMd ? "80px" : "70px", mb: "40px" }} text1="Кастомный тариф " text2="Опросник" />
<TariffConstructorCard
time="9 месяцев"
quantity="1000 шаблонов"
discountText="-60%"
totalText="3 190 руб."
totalWithoutDiscountText="10 190 руб."
/>
<Box
sx={{
display: "flex",
flexDirection: upMd ? "row" : "column",
mt: upMd ? "80px" : "70px",
pt: upMd ? "30px" : undefined,
borderTop: upMd ? `1px solid ${theme.palette.grey2.main}` : undefined,
}}
20 190 руб.
</Typography>
<Typography
variant="p1"
sx={{
fontWeight: 500,
fontSize: "26px",
lineHeight: "31px",
order: upMd ? 2 : 1,
}}
>
<Box
sx={{
width: upMd ? "68.5%" : undefined,
pr: upMd ? "15%" : undefined,
display: "flex",
flexWrap: "wrap",
flexDirection: "column",
}}
>
<Typography
variant="h4"
mb={upMd ? "18px" : "30px"}
>Итоговая цена</Typography>
<Typography color={theme.palette.grey3.main}>Текст-заполнитель
это текст, который имеет Текст-заполнитель
это текст, который имеет Текст-заполнитель
это текст, который имеет Текст-заполнитель
это текст, который имеет Текст-заполнитель</Typography>
</Box>
<Box
sx={{
color: theme.palette.grey3.main,
width: upMd ? "31.5%" : undefined,
pl: upMd ? "33px" : undefined,
}}
>
<Box
sx={{
display: "flex",
flexDirection: upMd ? "column" : "row",
alignItems: upMd ? "start" : "center",
mt: upMd ? "10px" : "30px",
gap: "15px",
}}
>
<Typography
color={theme.palette.orange.main}
sx={{
textDecoration: "line-through",
order: upMd ? 1 : 2,
}}
>20 190 руб.</Typography>
<Typography
variant="p1"
sx={{
fontWeight: 500,
fontSize: "26px",
lineHeight: "31px",
order: upMd ? 2 : 1,
}}
>6 380 руб.</Typography>
</Box>
<CustomButton
variant="contained"
sx={{
mt: "25px",
backgroundColor: theme.palette.brightPurple.main,
}}
>Выбрать</CustomButton>
</Box>
</Box>
</SectionWrapper >
);
}
6 380 руб.
</Typography>
</Box>
<CustomButton
variant="contained"
sx={{
mt: "25px",
backgroundColor: theme.palette.brightPurple.main,
}}
>
Выбрать
</CustomButton>
</Box>
</Box>
</SectionWrapper>
);
}

@ -1,33 +1,28 @@
import { Box } from "@mui/material";
import CustomAccordion from "../../components/CustomAccordion";
import CustomAccordion from "@components/CustomAccordion";
interface Props {
content: [string, string][];
content: [string, string][];
}
export default function AccordionWrapper({ content }: Props) {
return (
<Box
sx={{
overflow: "hidden",
borderRadius: "12px",
boxShadow: `0px 100px 309px rgba(210, 208, 225, 0.24),
return (
<Box
sx={{
overflow: "hidden",
borderRadius: "12px",
boxShadow: `0px 100px 309px rgba(210, 208, 225, 0.24),
0px 41.7776px 129.093px rgba(210, 208, 225, 0.172525),
0px 22.3363px 69.0192px rgba(210, 208, 225, 0.143066),
0px 12.5216px 38.6916px rgba(210, 208, 225, 0.12),
0px 6.6501px 20.5488px rgba(210, 208, 225, 0.0969343),
0px 2.76726px 8.55082px rgba(210, 208, 225, 0.0674749)`,
}}
>
{content.map((accordionItem, index) => (
<CustomAccordion
key={index}
header={accordionItem[0]}
text={accordionItem[1]}
/>
))}
</Box>
);
}}
>
{content.map((accordionItem, index) => (
<CustomAccordion key={index} header={accordionItem[0]} text={accordionItem[1]} />
))}
</Box>
);
}

@ -1,99 +1,102 @@
import { Box, Typography, useMediaQuery, useTheme } from "@mui/material";
import CustomButton from "../../components/CustomButton";
import PenaLogo from "../../components/PenaLogo";
import SectionWrapper from "../../components/SectionWrapper";
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"));
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",
}}
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)",
}}
>
<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)",
}}
>
<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 >
);
};
<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>
);
}

@ -1,8 +1,10 @@
import { Box, Typography, useMediaQuery, useTheme } from "@mui/material";
import CardWithLink from "../../components/CardWithLink";
import UnderlinedLink from "../../components/UnderlinedLink";
import SectionWrapper from "../../components/SectionWrapper";
import ArrowForwardIcon from "@mui/icons-material/ArrowForward";
import CardWithLink from "@components/CardWithLink";
import UnderlinedLink from "@components/UnderlinedLink";
import SectionWrapper from "@components/SectionWrapper";
import icon1 from "../../assets/animations/Icon_1.webm";
import icon2 from "../../assets/animations/Icon_2.webm";
import icon3 from "../../assets/animations/Icon_3.webm";
@ -10,110 +12,111 @@ 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"));
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",
}}
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%",
}}
>
<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>
);
}
Интеграции, избавляющие от рутины
</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>
);
}

@ -6,92 +6,93 @@ import cardPagesBackground3 from "../../assets/card-background/card-pages-backgr
import cardPagesBackground4 from "../../assets/card-background/card-pages-background4.png";
import cardPagesBackground5 from "../../assets/card-background/card-pages-background5.png";
import cardPagesBackground6 from "../../assets/card-background/card-pages-background6.png";
import UnderlinedLink from "../../components/UnderlinedLink";
import SectionWrapper from "../../components/SectionWrapper";
import UnderlinedLink from "@components/UnderlinedLink";
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",
}}
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",
}}
>
<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%" : 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>
);
}
Узнайте, как наши сервисы решают ваши задачи
</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>
);
}

@ -1,59 +1,34 @@
import { useMediaQuery, useTheme } from "@mui/material";
import SectionWrapper from "../../components/SectionWrapper";
import SectionWrapper from "@components/SectionWrapper";
import Infographics from "./Infographics";
export default function Section4() {
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const itemsFlex = upMd ? "1 0 33.333%" : "1 0 50%";
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",
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>
);
}
return (
<SectionWrapper
component="section"
maxWidth="lg"
outerContainerSx={{
backgroundColor: theme.palette.darkPurple.main,
}}
sx={{
display: "flex",
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>
);
}

@ -1,59 +1,56 @@
import { Box, Typography, useMediaQuery, useTheme } from "@mui/material";
import CustomButton from "../../components/CustomButton";
import SectionWrapper from "../../components/SectionWrapper";
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",
}}
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",
}}
>
<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 >
);
}
Сервисы помогают предпринимателям, маркетологам и агентствам сделать интернет-маркетинг прозрачным и
эффективным. С нами не придется тратить рекламный бюджет впустую и терять клиентов на сайте.
</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>
);
}

@ -1,131 +1,128 @@
import { Box, IconButton, Typography, useMediaQuery, useTheme } from "@mui/material";
import CustomButton from "../../components/CustomButton";
import SectionWrapper from "../../components/SectionWrapper";
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import CustomButton from "@components/CustomButton";
import SectionWrapper from "@components/SectionWrapper";
import ComplexNavText from "@components/ComplexNavText";
import PaymentMethodCard from "./PaymentMethodCard";
import mastercardLogo from "../../assets/bank-logo/logo-mastercard.png";
import visaLogo from "../../assets/bank-logo/logo-visa.png";
import qiwiLogo from "../../assets/bank-logo/logo-qiwi.png";
import mirLogo from "../../assets/bank-logo/logo-mir.png";
import tinkoffLogo from "../../assets/bank-logo/logo-tinkoff.png";
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import ComplexNavText from "../../components/ComplexNavText";
export default function Payment() {
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const upSm = useMediaQuery(theme.breakpoints.up("sm"));
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const upSm = useMediaQuery(theme.breakpoints.up("sm"));
return (
<SectionWrapper
maxWidth="lg"
sx={{
mt: "25px",
mb: "70px",
}}
>
{upMd &&
<ComplexNavText text1="Все тарифы — " text2="Способ оплаты" />
}
<Box
sx={{
mt: "20px",
mb: "40px",
display: "flex",
gap: "10px",
}}
>
{!upMd &&
<IconButton sx={{ p: 0, height: "28px", width: "28px", color: "black" }}>
<ArrowBackIcon />
</IconButton>
}
<Typography variant="h4">Способ оплаты</Typography>
</Box>
{!upMd &&
<Typography variant="body2" mb="30px">Выберите способ оплаты</Typography>
}
<Box
sx={{
backgroundColor: upMd ? "white" : undefined,
display: "flex",
flexDirection: upMd ? "row" : "column",
borderRadius: "12px",
boxShadow: upMd ? `0px 100px 309px rgba(210, 208, 225, 0.24),
return (
<SectionWrapper
maxWidth="lg"
sx={{
mt: "25px",
mb: "70px",
}}
>
{upMd && <ComplexNavText text1="Все тарифы — " text2="Способ оплаты" />}
<Box
sx={{
mt: "20px",
mb: "40px",
display: "flex",
gap: "10px",
}}
>
{!upMd && (
<IconButton sx={{ p: 0, height: "28px", width: "28px", color: "black" }}>
<ArrowBackIcon />
</IconButton>
)}
<Typography variant="h4">Способ оплаты</Typography>
</Box>
{!upMd && (
<Typography variant="body2" mb="30px">
Выберите способ оплаты
</Typography>
)}
<Box
sx={{
backgroundColor: upMd ? "white" : undefined,
display: "flex",
flexDirection: upMd ? "row" : "column",
borderRadius: "12px",
boxShadow: upMd
? `0px 100px 309px rgba(210, 208, 225, 0.24),
0px 41.7776px 129.093px rgba(210, 208, 225, 0.172525),
0px 22.3363px 69.0192px rgba(210, 208, 225, 0.143066),
0px 12.5216px 38.6916px rgba(210, 208, 225, 0.12),
0px 6.6501px 20.5488px rgba(210, 208, 225, 0.0969343),
0px 2.76726px 8.55082px rgba(210, 208, 225, 0.0674749)`
:
undefined,
}}
: undefined,
}}
>
<Box
sx={{
width: upMd ? "68.5%" : undefined,
p: upMd ? "20px" : undefined,
display: "flex",
flexDirection: upSm ? "row" : "column",
flexWrap: "wrap",
gap: upMd ? "14px" : "20px",
}}
>
<PaymentMethodCard name="Mastercard" image={mastercardLogo} />
<PaymentMethodCard name="Visa" image={visaLogo} />
<PaymentMethodCard name="QIWI Кошелек" image={qiwiLogo} />
<PaymentMethodCard name="Мир" image={mirLogo} />
<PaymentMethodCard name="Тинькофф" image={tinkoffLogo} />
</Box>
<Box
sx={{
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
alignItems: "start",
color: theme.palette.grey3.main,
width: upMd ? "31.5%" : undefined,
p: upMd ? "20px" : undefined,
pl: upMd ? "33px" : undefined,
mt: upMd ? undefined : "30px",
borderLeft: upMd ? `1px solid ${theme.palette.grey2.main}` : undefined,
}}
>
<Box
sx={{
display: "flex",
flexDirection: "column",
maxWidth: "85%",
}}
>
{upMd && <Typography mb="56px">Выберите способ оплаты</Typography>}
<Typography mb="20px">К оплате</Typography>
<Typography
sx={{
fontWeight: 500,
fontSize: "20px",
lineHeight: "24px",
mb: "28px",
}}
>
<Box
sx={{
width: upMd ? "68.5%" : undefined,
p: upMd ? "20px" : undefined,
display: "flex",
flexDirection: upSm ? "row" : "column",
flexWrap: "wrap",
gap: upMd ? "14px" : "20px",
}}
>
<PaymentMethodCard name="Mastercard" image={mastercardLogo} />
<PaymentMethodCard name="Visa" image={visaLogo} />
<PaymentMethodCard name="QIWI Кошелек" image={qiwiLogo} />
<PaymentMethodCard name="Мир" image={mirLogo} />
<PaymentMethodCard name="Тинькофф" image={tinkoffLogo} />
</Box>
<Box
sx={{
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
alignItems: "start",
color: theme.palette.grey3.main,
width: upMd ? "31.5%" : undefined,
p: upMd ? "20px" : undefined,
pl: upMd ? "33px" : undefined,
mt: upMd ? undefined : "30px",
borderLeft: upMd ? `1px solid ${theme.palette.grey2.main}` : undefined,
}}
>
<Box
sx={{
display: "flex",
flexDirection: "column",
maxWidth: "85%",
}}
>
{upMd &&
<Typography mb="56px">
Выберите способ оплаты
</Typography>
}
<Typography mb="20px">
К оплате
</Typography>
<Typography
sx={{
fontWeight: 500,
fontSize: "20px",
lineHeight: "24px",
mb: "28px",
}}
>
3 190 руб.
</Typography>
</Box>
<CustomButton
variant="outlined"
sx={{
borderColor: theme.palette.brightPurple.main,
mt: "auto",
}}
>Выбрать</CustomButton>
</Box>
</Box>
</SectionWrapper>
);
}
3 190 руб.
</Typography>
</Box>
<CustomButton
variant="outlined"
sx={{
borderColor: theme.palette.brightPurple.main,
mt: "auto",
}}
>
Выбрать
</CustomButton>
</Box>
</Box>
</SectionWrapper>
);
}

@ -1,177 +1,190 @@
import { Box, IconButton, Link, Typography, useMediaQuery, useTheme } from "@mui/material";
import CustomButton from "../components/CustomButton";
import InputTextfield from "../components/InputTextfield";
import PenaLogo from "../components/PenaLogo";
import CloseIcon from "@mui/icons-material/Close";
import { apiRequestHandler } from "../utils/api/apiRequestHandler";
import { useNavigate } from "react-router-dom";
import { useFormik } from "formik";
import { useSnackbar } from "notistack";
import { ApiError } from "../utils/api/types";
import CustomButton from "@components/CustomButton";
import InputTextfield from "@components/InputTextfield";
import PenaLogo from "@components/PenaLogo";
import { apiRequestHandler } from "@utils/api/apiRequestHandler";
import { ApiError } from "@utils/api/types";
import { useSnackbar } from "notistack";
interface Values {
email: string;
password: string;
email: string;
password: string;
}
// TODO
function validate(values: Values) {
const errors = {} as any;
if (!values.email) {
errors.email = "Required";
}
if (!values.password) {
errors.password = "Required";
} else if (!/^[\w-]{8,25}$/i.test(values.password)) {
errors.password = "Invalid password";
}
return errors;
const errors = {} as any;
if (!values.email) {
errors.email = "Required";
}
if (!values.password) {
errors.password = "Required";
} else if (!/^[\w-]{8,25}$/i.test(values.password)) {
errors.password = "Invalid password";
}
return errors;
}
export default function Signin() {
const { enqueueSnackbar } = useSnackbar();
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const navigate = useNavigate();
const formik = useFormik<Values>({
initialValues: {
email: "",
password: "",
},
validate,
onSubmit: async (values: Values) => {
const result = await apiRequestHandler.login({
email: values.email,
password: values.password,
});
if (result instanceof ApiError) {
enqueueSnackbar(`Error: ${result.message}`);
} else if (result instanceof Error) {
console.log(result);
enqueueSnackbar(`Unknown error`);
} else {
// navigate("/"); // TODO
}
}
});
const { enqueueSnackbar } = useSnackbar();
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const navigate = useNavigate();
const formik = useFormik<Values>({
initialValues: {
email: "",
password: "",
},
validate,
onSubmit: async (values: Values) => {
const result = await apiRequestHandler.login({
email: values.email,
password: values.password,
});
if (result instanceof ApiError) {
enqueueSnackbar(`Error: ${result.message}`);
} else if (result instanceof Error) {
console.log(result);
enqueueSnackbar(`Unknown error`);
} else {
// navigate("/"); // TODO
}
},
});
function handleClose() {
navigate("/");
}
function handleClose() {
navigate("/");
}
return (
<Box
sx={{
display: "flex",
justifyContent: "center",
height: upMd ? undefined : "100vh",
}}
>
<Box
component="form"
onSubmit={formik.handleSubmit}
noValidate
sx={{
position: "relative",
width: upMd ? "600px" : "100%",
height: upMd ? "auto" : "100%",
backgroundColor: "white",
display: "flex",
alignItems: "center",
flexDirection: "column",
mt: upMd ? "184px" : undefined,
p: upMd ? "50px" : "18px",
pb: upMd ? "40px" : "30px",
gap: "15px",
borderRadius: "12px",
boxShadow: `0px 100px 309px rgba(210, 208, 225, 0.24),
return (
<Box
sx={{
display: "flex",
justifyContent: "center",
height: upMd ? undefined : "100vh",
}}
>
<Box
component="form"
onSubmit={formik.handleSubmit}
noValidate
sx={{
position: "relative",
width: upMd ? "600px" : "100%",
height: upMd ? "auto" : "100%",
backgroundColor: "white",
display: "flex",
alignItems: "center",
flexDirection: "column",
mt: upMd ? "184px" : undefined,
p: upMd ? "50px" : "18px",
pb: upMd ? "40px" : "30px",
gap: "15px",
borderRadius: "12px",
boxShadow: `0px 100px 309px rgba(210, 208, 225, 0.24),
0px 41.7776px 129.093px rgba(210, 208, 225, 0.172525),
0px 22.3363px 69.0192px rgba(210, 208, 225, 0.143066),
0px 12.5216px 38.6916px rgba(210, 208, 225, 0.12),
0px 6.6501px 20.5488px rgba(210, 208, 225, 0.0969343),
0px 2.76726px 8.55082px rgba(210, 208, 225, 0.0674749)`,
}}
>
<IconButton
onClick={handleClose}
sx={{
position: "absolute",
right: "7px",
top: "7px",
}}>
<CloseIcon sx={{ transform: "scale(1.5)" }} />
</IconButton>
<Box sx={{ mt: "65px" }}>
<PenaLogo width={upMd ? 233 : 196} />
</Box>
<Typography
sx={{
color: theme.palette.grey3.main,
mt: "5px",
mb: upMd ? "35px" : "33px",
}}
>Вход в личный кабинет</Typography>
<InputTextfield
TextfieldProps={{
value: formik.values.email,
placeholder: "+7 900 000 00 00 или username@penahub.com",
onBlur: formik.handleBlur,
error: formik.touched.email && Boolean(formik.errors.email),
helperText: formik.touched.email && formik.errors.email,
}}
onChange={formik.handleChange}
id="email"
label="Телефон или E-mail"
gap={upMd ? "15px" : "10px"}
/>
<InputTextfield
TextfieldProps={{
value: formik.values.password,
placeholder: "Не менее 8 символов",
onBlur: formik.handleBlur,
error: formik.touched.password && Boolean(formik.errors.password),
helperText: formik.touched.password && formik.errors.password,
type: "password",
}}
onChange={formik.handleChange}
id="password"
label="Пароль"
gap={upMd ? "15px" : "10px"}
/>
<CustomButton
fullWidth
variant="contained"
sx={{
backgroundColor: theme.palette.brightPurple.main,
textColor: "white",
width: "100%",
py: "12px",
mt: upMd ? undefined : "10px",
}}
type="submit"
disabled={formik.isSubmitting}
>Войти</CustomButton>
<Link href="#"
sx={{
color: theme.palette.grey3.main,
mb: "30px",
mt: upMd ? undefined : "5px",
}}
>Забыли пароль?</Link>
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: "10px",
mt: "auto",
}}
>
<Typography sx={{ color: theme.palette.brightPurple.main }}>Вы еще не присоединились?</Typography>
<Link href="#" sx={{ color: theme.palette.brightPurple.main }}>Регистрация</Link>
</Box>
</Box>
}}
>
<IconButton
onClick={handleClose}
sx={{
position: "absolute",
right: "7px",
top: "7px",
}}
>
<CloseIcon sx={{ transform: "scale(1.5)" }} />
</IconButton>
<Box sx={{ mt: "65px" }}>
<PenaLogo width={upMd ? 233 : 196} />
</Box>
);
}
<Typography
sx={{
color: theme.palette.grey3.main,
mt: "5px",
mb: upMd ? "35px" : "33px",
}}
>
Вход в личный кабинет
</Typography>
<InputTextfield
TextfieldProps={{
value: formik.values.email,
placeholder: "+7 900 000 00 00 или username@penahub.com",
onBlur: formik.handleBlur,
error: formik.touched.email && Boolean(formik.errors.email),
helperText: formik.touched.email && formik.errors.email,
}}
onChange={formik.handleChange}
id="email"
label="Телефон или E-mail"
gap={upMd ? "15px" : "10px"}
/>
<InputTextfield
TextfieldProps={{
value: formik.values.password,
placeholder: "Не менее 8 символов",
onBlur: formik.handleBlur,
error: formik.touched.password && Boolean(formik.errors.password),
helperText: formik.touched.password && formik.errors.password,
type: "password",
}}
onChange={formik.handleChange}
id="password"
label="Пароль"
gap={upMd ? "15px" : "10px"}
/>
<CustomButton
fullWidth
variant="contained"
sx={{
backgroundColor: theme.palette.brightPurple.main,
textColor: "white",
width: "100%",
py: "12px",
mt: upMd ? undefined : "10px",
}}
type="submit"
disabled={formik.isSubmitting}
>
Войти
</CustomButton>
<Link
href="#"
sx={{
color: theme.palette.grey3.main,
mb: "30px",
mt: upMd ? undefined : "5px",
}}
>
Забыли пароль?
</Link>
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: "10px",
mt: "auto",
}}
>
<Typography sx={{ color: theme.palette.brightPurple.main }}>Вы еще не присоединились?</Typography>
<Link href="#" sx={{ color: theme.palette.brightPurple.main }}>
Регистрация
</Link>
</Box>
</Box>
</Box>
);
}

@ -1,221 +1,228 @@
import { Box, IconButton, Link, Typography, useMediaQuery, useTheme } from "@mui/material";
import CustomButton from "../components/CustomButton";
import InputTextfield from "../components/InputTextfield";
import PenaLogo from "../components/PenaLogo";
import CloseIcon from "@mui/icons-material/Close";
import { apiRequestHandler } from "../utils/api/apiRequestHandler";
import { apiRequestHandler } from "@utils/api/apiRequestHandler";
import { useNavigate } from "react-router-dom";
import { useFormik } from "formik";
import { useSnackbar } from "notistack";
import { ApiError } from "../utils/api/types";
import CloseIcon from "@mui/icons-material/Close";
import CustomButton from "@components/CustomButton";
import InputTextfield from "@components/InputTextfield";
import PenaLogo from "@components/PenaLogo";
import { ApiError } from "@utils/api/types";
import { useSnackbar } from "notistack";
interface Values {
login: string;
email: string;
phoneNumber: string;
password: string;
repeatPassword: string;
login: string;
email: string;
phoneNumber: string;
password: string;
repeatPassword: string;
}
// TODO
function validate(values: Values) {
const errors = {} as any;
if (!values.login) {
errors.login = "Required";
} else if (!/^[\w-]{3,25}$/i.test(values.login)) {
errors.login = "Invalid username";
}
if (!values.password) {
errors.password = "Required";
} else if (!/^[\w-]{8,25}$/i.test(values.password)) {
errors.password = "Invalid password";
}
if (values.password !== values.repeatPassword) {
errors.repeatPassword = "Passwords do not match";
}
return errors;
const errors = {} as any;
if (!values.login) {
errors.login = "Required";
} else if (!/^[\w-]{3,25}$/i.test(values.login)) {
errors.login = "Invalid username";
}
if (!values.password) {
errors.password = "Required";
} else if (!/^[\w-]{8,25}$/i.test(values.password)) {
errors.password = "Invalid password";
}
if (values.password !== values.repeatPassword) {
errors.repeatPassword = "Passwords do not match";
}
return errors;
}
export default function Signup() {
const { enqueueSnackbar } = useSnackbar();
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const navigate = useNavigate();
const formik = useFormik<Values>({
initialValues: {
login: "",
email: "",
phoneNumber: "",
password: "",
repeatPassword: "",
},
validate,
onSubmit: async (values: Values) => {
const result = await apiRequestHandler.register({
email: values.email,
login: values.login,
password: values.password,
phoneNumber: values.phoneNumber,
});
if (result instanceof ApiError) {
enqueueSnackbar(`Error: ${result.message}`);
return;
} else if (result instanceof Error) {
console.log(result);
enqueueSnackbar(`Unknown error`);
return;
} else {
// navigate("/signin"); // TODO
}
}
});
const { enqueueSnackbar } = useSnackbar();
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const navigate = useNavigate();
const formik = useFormik<Values>({
initialValues: {
login: "",
email: "",
phoneNumber: "",
password: "",
repeatPassword: "",
},
validate,
onSubmit: async (values: Values) => {
const result = await apiRequestHandler.register({
email: values.email,
login: values.login,
password: values.password,
phoneNumber: values.phoneNumber,
});
if (result instanceof ApiError) {
enqueueSnackbar(`Error: ${result.message}`);
return;
} else if (result instanceof Error) {
console.log(result);
enqueueSnackbar(`Unknown error`);
return;
} else {
// navigate("/signin"); // TODO
}
},
});
function handleClose() {
navigate("/");
}
function handleClose() {
navigate("/");
}
return (
<Box
sx={{
display: "flex",
justifyContent: "center",
height: upMd ? undefined : "100vh",
}}
>
<Box
component="form"
onSubmit={formik.handleSubmit}
noValidate
sx={{
position: "relative",
width: upMd ? "600px" : "100%",
height: upMd ? "auto" : "100%",
backgroundColor: "white",
display: "flex",
alignItems: "center",
flexDirection: "column",
mt: upMd ? "79px" : undefined,
p: upMd ? "50px" : "18px",
pb: upMd ? "40px" : "30px",
gap: "15px",
borderRadius: "12px",
boxShadow: `0px 100px 309px rgba(210, 208, 225, 0.24),
return (
<Box
sx={{
display: "flex",
justifyContent: "center",
height: upMd ? undefined : "100vh",
}}
>
<Box
component="form"
onSubmit={formik.handleSubmit}
noValidate
sx={{
position: "relative",
width: upMd ? "600px" : "100%",
height: upMd ? "auto" : "100%",
backgroundColor: "white",
display: "flex",
alignItems: "center",
flexDirection: "column",
mt: upMd ? "79px" : undefined,
p: upMd ? "50px" : "18px",
pb: upMd ? "40px" : "30px",
gap: "15px",
borderRadius: "12px",
boxShadow: `0px 100px 309px rgba(210, 208, 225, 0.24),
0px 41.7776px 129.093px rgba(210, 208, 225, 0.172525),
0px 22.3363px 69.0192px rgba(210, 208, 225, 0.143066),
0px 12.5216px 38.6916px rgba(210, 208, 225, 0.12),
0px 6.6501px 20.5488px rgba(210, 208, 225, 0.0969343),
0px 2.76726px 8.55082px rgba(210, 208, 225, 0.0674749)`,
}}
>
<IconButton
onClick={handleClose}
sx={{
position: "absolute",
right: "7px",
top: "7px",
}}
>
<CloseIcon sx={{ transform: "scale(1.5)" }} />
</IconButton>
<Box sx={{ mt: "65px" }}>
<PenaLogo width={upMd ? 233 : 196} />
</Box>
<Typography
sx={{
color: theme.palette.grey3.main,
mt: "5px",
mb: upMd ? "35px" : "33px",
}}
>Регистрация</Typography>
<InputTextfield
TextfieldProps={{
value: formik.values.login,
placeholder: "username",
onBlur: formik.handleBlur,
error: formik.touched.login && Boolean(formik.errors.login),
helperText: formik.touched.login && formik.errors.login,
}}
onChange={formik.handleChange}
id="login"
label="Login"
gap={upMd ? "15px" : "10px"}
/>
<InputTextfield
TextfieldProps={{
value: formik.values.email,
placeholder: "username@penahub.com",
onBlur: formik.handleBlur,
error: formik.touched.email && Boolean(formik.errors.email),
helperText: formik.touched.email && formik.errors.email,
}}
onChange={formik.handleChange}
id="email"
label="E-mail"
gap={upMd ? "15px" : "10px"}
/>
<InputTextfield
TextfieldProps={{
value: formik.values.phoneNumber,
placeholder: "+7 900 000 00 00",
onBlur: formik.handleBlur,
error: formik.touched.phoneNumber && Boolean(formik.errors.phoneNumber),
helperText: formik.touched.phoneNumber && formik.errors.phoneNumber,
}}
onChange={formik.handleChange}
id="phoneNumber"
label="Телефон"
gap={upMd ? "15px" : "10px"}
/>
<InputTextfield
TextfieldProps={{
value: formik.values.password,
placeholder: "Не менее 8 символов",
onBlur: formik.handleBlur,
error: formik.touched.password && Boolean(formik.errors.password),
helperText: formik.touched.password && formik.errors.password,
type: "password",
}}
onChange={formik.handleChange}
id="password"
label="Пароль"
gap={upMd ? "15px" : "10px"}
/>
<InputTextfield
TextfieldProps={{
value: formik.values.repeatPassword,
placeholder: "Не менее 8 символов",
onBlur: formik.handleBlur,
error: formik.touched.repeatPassword && Boolean(formik.errors.repeatPassword),
helperText: formik.touched.repeatPassword && formik.errors.repeatPassword,
type: "password",
}}
onChange={formik.handleChange}
id="repeatPassword"
label="Повторить пароль"
gap={upMd ? "15px" : "10px"}
/>
<CustomButton
fullWidth
variant="contained"
sx={{
backgroundColor: theme.palette.brightPurple.main,
textColor: "white",
width: "100%",
py: "12px",
mt: upMd ? undefined : "10px",
}}
type="submit"
disabled={formik.isSubmitting}
>Войти</CustomButton>
<Link
href="#"
sx={{
color: theme.palette.brightPurple.main,
mt: "auto",
}}
>Вход в личный кабинет</Link>
</Box>
}}
>
<IconButton
onClick={handleClose}
sx={{
position: "absolute",
right: "7px",
top: "7px",
}}
>
<CloseIcon sx={{ transform: "scale(1.5)" }} />
</IconButton>
<Box sx={{ mt: "65px" }}>
<PenaLogo width={upMd ? 233 : 196} />
</Box>
);
}
<Typography
sx={{
color: theme.palette.grey3.main,
mt: "5px",
mb: upMd ? "35px" : "33px",
}}
>
Регистрация
</Typography>
<InputTextfield
TextfieldProps={{
value: formik.values.login,
placeholder: "username",
onBlur: formik.handleBlur,
error: formik.touched.login && Boolean(formik.errors.login),
helperText: formik.touched.login && formik.errors.login,
}}
onChange={formik.handleChange}
id="login"
label="Login"
gap={upMd ? "15px" : "10px"}
/>
<InputTextfield
TextfieldProps={{
value: formik.values.email,
placeholder: "username@penahub.com",
onBlur: formik.handleBlur,
error: formik.touched.email && Boolean(formik.errors.email),
helperText: formik.touched.email && formik.errors.email,
}}
onChange={formik.handleChange}
id="email"
label="E-mail"
gap={upMd ? "15px" : "10px"}
/>
<InputTextfield
TextfieldProps={{
value: formik.values.phoneNumber,
placeholder: "+7 900 000 00 00",
onBlur: formik.handleBlur,
error: formik.touched.phoneNumber && Boolean(formik.errors.phoneNumber),
helperText: formik.touched.phoneNumber && formik.errors.phoneNumber,
}}
onChange={formik.handleChange}
id="phoneNumber"
label="Телефон"
gap={upMd ? "15px" : "10px"}
/>
<InputTextfield
TextfieldProps={{
value: formik.values.password,
placeholder: "Не менее 8 символов",
onBlur: formik.handleBlur,
error: formik.touched.password && Boolean(formik.errors.password),
helperText: formik.touched.password && formik.errors.password,
type: "password",
}}
onChange={formik.handleChange}
id="password"
label="Пароль"
gap={upMd ? "15px" : "10px"}
/>
<InputTextfield
TextfieldProps={{
value: formik.values.repeatPassword,
placeholder: "Не менее 8 символов",
onBlur: formik.handleBlur,
error: formik.touched.repeatPassword && Boolean(formik.errors.repeatPassword),
helperText: formik.touched.repeatPassword && formik.errors.repeatPassword,
type: "password",
}}
onChange={formik.handleChange}
id="repeatPassword"
label="Повторить пароль"
gap={upMd ? "15px" : "10px"}
/>
<CustomButton
fullWidth
variant="contained"
sx={{
backgroundColor: theme.palette.brightPurple.main,
textColor: "white",
width: "100%",
py: "12px",
mt: upMd ? undefined : "10px",
}}
type="submit"
disabled={formik.isSubmitting}
>
Войти
</CustomButton>
<Link
href="#"
sx={{
color: theme.palette.brightPurple.main,
mt: "auto",
}}
>
Вход в личный кабинет
</Link>
</Box>
</Box>
);
}

@ -1,137 +1,144 @@
import { Box, Typography, FormControl, InputBase, useMediaQuery, useTheme } from "@mui/material";
import { useState } from "react";
import CustomButton from "../../components/CustomButton";
import { apiRequestHandler } from "../../utils/api/apiRequestHandler";
import { useSnackbar } from "notistack";
import { useNavigate } from "react-router-dom";
import { ApiError } from "../../utils/api/types";
import CustomButton from "@components/CustomButton";
import { apiRequestHandler } from "@utils/api/apiRequestHandler";
import { ApiError } from "@utils/api/types";
import { useSnackbar } from "notistack";
export default function CreateTicket() {
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const { enqueueSnackbar } = useSnackbar();
const navigate = useNavigate();
const [ticketName, setTicketName] = useState<string>("");
const [ticketBody, setTicketBody] = useState<string>("");
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const { enqueueSnackbar } = useSnackbar();
const navigate = useNavigate();
const [ticketName, setTicketName] = useState<string>("");
const [ticketBody, setTicketBody] = useState<string>("");
async function handleCreateTicket() {
const result = await apiRequestHandler.createTicket({
Title: ticketName,
Message: ticketBody,
});
if (result instanceof ApiError) {
enqueueSnackbar(`Error: ${result.message}`);
} else if (result instanceof Error) {
console.log(result);
enqueueSnackbar(`Unknown error`);
} else {
navigate(`/support/${result.Ticket}`);
}
async function handleCreateTicket() {
const result = await apiRequestHandler.createTicket({
Title: ticketName,
Message: ticketBody,
});
if (result instanceof ApiError) {
enqueueSnackbar(`Error: ${result.message}`);
} else if (result instanceof Error) {
console.log(result);
enqueueSnackbar(`Unknown error`);
} else {
navigate(`/support/${result.Ticket}`);
}
}
return (
<Box
sx={{
backgroundColor: upMd ? "white" : undefined,
display: "flex",
flexDirection: upMd ? "row" : "column",
maxHeight: upMd ? "443px" : undefined,
borderRadius: "12px",
p: upMd ? "20px" : undefined,
gap: upMd ? "40px" : "20px",
boxShadow: upMd ?
`0px 100px 309px rgba(210, 208, 225, 0.24),
return (
<Box
sx={{
backgroundColor: upMd ? "white" : undefined,
display: "flex",
flexDirection: upMd ? "row" : "column",
maxHeight: upMd ? "443px" : undefined,
borderRadius: "12px",
p: upMd ? "20px" : undefined,
gap: upMd ? "40px" : "20px",
boxShadow: upMd
? `0px 100px 309px rgba(210, 208, 225, 0.24),
0px 41.7776px 129.093px rgba(210, 208, 225, 0.172525),
0px 22.3363px 69.0192px rgba(210, 208, 225, 0.143066),
0px 12.5216px 38.6916px rgba(210, 208, 225, 0.12),
0px 6.6501px 20.5488px rgba(210, 208, 225, 0.0969343),
0px 2.76726px 8.55082px rgba(210, 208, 225, 0.0674749)`
:
undefined,
: undefined,
}}
>
<Box
sx={{
display: "flex",
alignItems: "start",
flexDirection: "column",
flexGrow: 1,
}}
>
<Typography variant={upMd ? "h5" : "body2"} mb={upMd ? "30px" : "20px"}>
Написать обращение
</Typography>
<FormControl sx={{ width: "100%" }}>
<InputBase
value={ticketName}
fullWidth
placeholder="Заголовок обращения"
id="ticket-header"
sx={{
backgroundColor: theme.palette.background.default,
border: `1px solid ${theme.palette.grey2.main}`,
borderRadius: "10px",
p: 0,
}}
inputProps={{
sx: {
borderRadius: "10px",
fontWeight: 400,
fontSize: "16px",
lineHeight: "19px",
pt: "10px",
pb: "10px",
px: "19px",
},
}}
onChange={(e) => setTicketName(e.target.value)}
/>
</FormControl>
<FormControl sx={{ width: "100%" }}>
<Box
sx={{
overflow: "hidden",
mt: "16px",
backgroundColor: theme.palette.background.default,
border: `1px solid ${theme.palette.grey2.main}`,
borderRadius: "10px",
}}
>
<InputBase
value={ticketBody}
fullWidth
placeholder="Текст обращения"
id="ticket-body"
multiline
sx={{
p: 0,
height: "284px",
alignItems: "start",
overflow: "auto",
}}
inputProps={{
sx: {
borderRadius: "10px",
fontWeight: 400,
fontSize: "16px",
lineHeight: "19px",
pt: "13px",
pb: "13px",
px: "19px",
height: "300px",
},
}}
onChange={(e) => setTicketBody(e.target.value)}
/>
</Box>
</FormControl>
</Box>
<Box sx={{ alignSelf: upMd ? "end" : "start" }}>
<CustomButton
onClick={handleCreateTicket}
variant="contained"
sx={{
backgroundColor: theme.palette.brightPurple.main,
}}
>
<Box
sx={{
display: "flex",
alignItems: "start",
flexDirection: "column",
flexGrow: 1,
}}
>
<Typography variant={upMd ? "h5" : "body2"} mb={upMd ? "30px" : "20px"}>Написать обращение</Typography>
<FormControl sx={{ width: "100%" }}>
<InputBase
value={ticketName}
fullWidth
placeholder="Заголовок обращения"
id="ticket-header"
sx={{
backgroundColor: theme.palette.background.default,
border: `1px solid ${theme.palette.grey2.main}`,
borderRadius: "10px",
p: 0,
}}
inputProps={{
sx: {
borderRadius: "10px",
fontWeight: 400,
fontSize: "16px",
lineHeight: "19px",
pt: "10px",
pb: "10px",
px: "19px",
}
}}
onChange={e => setTicketName(e.target.value)}
/>
</FormControl>
<FormControl sx={{ width: "100%" }}>
<Box sx={{
overflow: "hidden",
mt: "16px",
backgroundColor: theme.palette.background.default,
border: `1px solid ${theme.palette.grey2.main}`,
borderRadius: "10px",
}}>
<InputBase
value={ticketBody}
fullWidth
placeholder="Текст обращения"
id="ticket-body"
multiline
sx={{
p: 0,
height: "284px",
alignItems: "start",
overflow: "auto",
}}
inputProps={{
sx: {
borderRadius: "10px",
fontWeight: 400,
fontSize: "16px",
lineHeight: "19px",
pt: "13px",
pb: "13px",
px: "19px",
height: "300px",
}
}}
onChange={e => setTicketBody(e.target.value)}
/>
</Box>
</FormControl>
</Box>
<Box sx={{ alignSelf: upMd ? "end" : "start" }}>
<CustomButton
onClick={handleCreateTicket}
variant="contained"
sx={{
backgroundColor: theme.palette.brightPurple.main,
}}
>Отправить</CustomButton>
</Box>
</Box >
);
}
Отправить
</CustomButton>
</Box>
</Box>
);
}

@ -1,59 +1,58 @@
import { Typography, Box, useTheme, useMediaQuery, IconButton } from "@mui/material";
import SectionWrapper from "../../components/SectionWrapper";
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import ComplexNavText from "../../components/ComplexNavText";
import { useParams } from "react-router-dom";
import SectionWrapper from "@components/SectionWrapper";
import ComplexNavText from "@components/ComplexNavText";
import SupportChat from "./SupportChat";
import CreateTicket from "./CreateTicket";
import { useParams } from "react-router-dom";
import TicketList from "./TicketList";
export default function Support() {
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const ticketId = useParams().ticketId;
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const ticketId = useParams().ticketId;
return (
<SectionWrapper
maxWidth="lg"
sx={{
pt: upMd ? "25px" : "20px",
pb: upMd ? "82px" : "43px",
height: "100%",
}}
return (
<SectionWrapper
maxWidth="lg"
sx={{
pt: upMd ? "25px" : "20px",
pb: upMd ? "82px" : "43px",
height: "100%",
}}
>
{upMd && <ComplexNavText text1="Все тарифы — " text2="Запрос в службу техподдержки" />}
<Box
sx={{
mt: "20px",
mb: "40px",
display: "flex",
gap: "10px",
}}
>
{!upMd && (
<IconButton sx={{ p: 0, height: "28px", width: "28px", color: "black" }}>
<ArrowBackIcon />
</IconButton>
)}
<Typography variant="h4">Запрос в службу техподдержки</Typography>
</Box>
{ticketId ? (
<SupportChat />
) : (
<Box
sx={{
display: "flex",
flexDirection: "column",
gap: upMd ? "40px" : "60px",
}}
>
{upMd &&
<ComplexNavText text1="Все тарифы — " text2="Запрос в службу техподдержки" />
}
<Box
sx={{
mt: "20px",
mb: "40px",
display: "flex",
gap: "10px",
}}
>
{!upMd &&
<IconButton sx={{ p: 0, height: "28px", width: "28px", color: "black" }}>
<ArrowBackIcon />
</IconButton>
}
<Typography variant="h4">Запрос в службу техподдержки</Typography>
</Box>
{ticketId ?
<SupportChat />
:
<Box
sx={{
display: "flex",
flexDirection: "column",
gap: upMd ? "40px" : "60px",
}}
>
<CreateTicket />
<TicketList />
</Box>
}
</SectionWrapper>
);
}
<CreateTicket />
<TicketList />
</Box>
)}
</SectionWrapper>
);
}

@ -1,269 +1,309 @@
import { Box, Fab, FormControl, IconButton, InputAdornment, InputBase, Typography, useMediaQuery, useTheme } from "@mui/material";
import { useEffect, useRef, useState } from "react";
import CustomButton from "../../components/CustomButton";
import Message from "./Message";
import SendIcon from "../../components/icons/SendIcon";
import { apiRequestHandler } from "../../utils/api/apiRequestHandler";
import { useParams } from "react-router-dom";
import { ApiError, TicketMessage } from "../../utils/api/types";
import { useSnackbar } from "notistack";
import {
Box,
Fab,
FormControl,
IconButton,
InputAdornment,
InputBase,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
import ArrowDownwardIcon from "@mui/icons-material/ArrowDownward";
import { throttle } from "../../utils/decorators";
import { useEffect, useRef, useState } from "react";
import { useParams } from "react-router-dom";
import CustomButton from "@components/CustomButton";
import SendIcon from "@components/icons/SendIcon";
import Message from "./Message";
import { apiRequestHandler } from "@utils/api/apiRequestHandler";
import { ApiError, TicketMessage } from "@utils/api/types";
import { throttle } from "@utils/decorators";
import { useSnackbar } from "notistack";
export default function SupportChat() {
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const [messageText, setMessageText] = useState<string>("");
const [messages, setMessages] = useState<TicketMessage[]>([]);
const ticketId = useParams().ticketId;
const { enqueueSnackbar } = useSnackbar();
const chatBoxRef = useRef<HTMLDivElement>();
const [isPreventAutoscroll, setIsPreventAutoscroll] = useState<boolean>(false);
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const [messageText, setMessageText] = useState<string>("");
const [messages, setMessages] = useState<TicketMessage[]>([]);
const ticketId = useParams().ticketId;
const { enqueueSnackbar } = useSnackbar();
const chatBoxRef = useRef<HTMLDivElement>();
const [isPreventAutoscroll, setIsPreventAutoscroll] = useState<boolean>(false);
function scrollToBottom() {
if (!chatBoxRef.current) return;
function scrollToBottom() {
if (!chatBoxRef.current) return;
chatBoxRef.current.scroll({
left: 0,
top: chatBoxRef.current.scrollHeight,
behavior: "smooth",
});
}
chatBoxRef.current.scroll({
left: 0,
top: chatBoxRef.current.scrollHeight,
behavior: "smooth",
});
}
useEffect(function refreshChatScrollTop() {
if (!chatBoxRef.current) return;
useEffect(function refreshChatScrollTop() {
if (!chatBoxRef.current) return;
const chatBox = chatBoxRef.current;
const scrollHandler = () => setIsPreventAutoscroll(chatBox.scrollTop + chatBox.clientHeight * 2 < chatBox.scrollHeight);
const chatBox = chatBoxRef.current;
const scrollHandler = () =>
setIsPreventAutoscroll(chatBox.scrollTop + chatBox.clientHeight * 2 < chatBox.scrollHeight);
const throttledScrollHandler = throttle(scrollHandler, 200);
chatBox.addEventListener("scroll", throttledScrollHandler);
const throttledScrollHandler = throttle(scrollHandler, 200);
chatBox.addEventListener("scroll", throttledScrollHandler);
return () => {
chatBox.removeEventListener("scroll", throttledScrollHandler);
};
}, []);
return () => {
chatBox.removeEventListener("scroll", throttledScrollHandler);
};
}, []);
// TODO При подписке на SSE сервер уже отправляет все сообщения тикета
useEffect(function getMessages() {
if (!ticketId) return;
// TODO При подписке на SSE сервер уже отправляет все сообщения тикета
useEffect(
function getMessages() {
if (!ticketId) return;
const abortController = new AbortController();
apiRequestHandler.getMessages({
const abortController = new AbortController();
apiRequestHandler
.getMessages(
{
amt: 100,
page: 0,
srch: "",
ticket: ticketId,
}, abortController.signal).then(result => {
if (result instanceof ApiError) {
enqueueSnackbar(`Api error: ${result.message}`);
} else if (result instanceof Error) {
enqueueSnackbar(`Error: ${result.message}`);
} else {
setMessages(result);
}
});
return () => {
abortController.abort();
};
}, [enqueueSnackbar, ticketId]);
useEffect(function scrollOnMessage() {
if (!chatBoxRef.current) return;
if (!isPreventAutoscroll) scrollToBottom();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [messages]);
useEffect(function subscribeToMessages() {
if (!ticketId) return;
const unsubscribe = apiRequestHandler.subscribeToTicket({
ticketId,
onMessage(event) {
// console.log("SSE received:", event.data);
try {
const newMessage = JSON.parse(event.data) as TicketMessage;
setMessages(prev => prev.findIndex(message => message.id === newMessage.id) === -1 ? [...prev.slice(), newMessage] : prev);
} catch (error) {
console.log("SSE is not JSON", error);
}
},
onError(event) {
console.log("SSE Error:", event);
},
});
return () => {
unsubscribe();
};
}, [ticketId]);
async function handleSendMessage() {
if (!ticketId) return;
const result = await apiRequestHandler.sendTicketMessage({
ticket: ticketId,
message: messageText,
lang: "ru",
files: [],
});
if (result instanceof ApiError) {
},
abortController.signal
)
.then((result) => {
if (result instanceof ApiError) {
enqueueSnackbar(`Api error: ${result.message}`);
} else if (result instanceof Error) {
} else if (result instanceof Error) {
enqueueSnackbar(`Error: ${result.message}`);
} else {
setMessageText("");
}
}
} else {
setMessages(result);
}
});
return (
<Box
sx={{
backgroundColor: upMd ? "white" : undefined,
display: "flex",
flexGrow: 1,
maxHeight: upMd ? "443px" : undefined,
borderRadius: "12px",
p: upMd ? "20px" : undefined,
gap: "40px",
boxShadow: upMd ?
`0px 100px 309px rgba(210, 208, 225, 0.24),
return () => {
abortController.abort();
};
},
[enqueueSnackbar, ticketId]
);
useEffect(
function scrollOnMessage() {
if (!chatBoxRef.current) return;
if (!isPreventAutoscroll) scrollToBottom();
// eslint-disable-next-line react-hooks/exhaustive-deps
},
[messages]
);
useEffect(
function subscribeToMessages() {
if (!ticketId) return;
const unsubscribe = apiRequestHandler.subscribeToTicket({
ticketId,
onMessage(event) {
// console.log("SSE received:", event.data);
try {
const newMessage = JSON.parse(event.data) as TicketMessage;
setMessages((prev) =>
prev.findIndex((message) => message.id === newMessage.id) === -1 ? [...prev.slice(), newMessage] : prev
);
} catch (error) {
console.log("SSE is not JSON", error);
}
},
onError(event) {
console.log("SSE Error:", event);
},
});
return () => {
unsubscribe();
};
},
[ticketId]
);
async function handleSendMessage() {
if (!ticketId) return;
const result = await apiRequestHandler.sendTicketMessage({
ticket: ticketId,
message: messageText,
lang: "ru",
files: [],
});
if (result instanceof ApiError) {
enqueueSnackbar(`Api error: ${result.message}`);
} else if (result instanceof Error) {
enqueueSnackbar(`Error: ${result.message}`);
} else {
setMessageText("");
}
}
return (
<Box
sx={{
backgroundColor: upMd ? "white" : undefined,
display: "flex",
flexGrow: 1,
maxHeight: upMd ? "443px" : undefined,
borderRadius: "12px",
p: upMd ? "20px" : undefined,
gap: "40px",
boxShadow: upMd
? `0px 100px 309px rgba(210, 208, 225, 0.24),
0px 41.7776px 129.093px rgba(210, 208, 225, 0.172525),
0px 22.3363px 69.0192px rgba(210, 208, 225, 0.143066),
0px 12.5216px 38.6916px rgba(210, 208, 225, 0.12),
0px 6.6501px 20.5488px rgba(210, 208, 225, 0.0969343),
0px 2.76726px 8.55082px rgba(210, 208, 225, 0.0674749)`
:
undefined,
}}
: undefined,
}}
>
<Box
sx={{
display: "flex",
alignItems: "start",
flexDirection: "column",
flexGrow: 1,
}}
>
<Typography variant={upMd ? "h5" : "body2"} mb={"4px"}>
Заголовок
</Typography>
<Typography
sx={{
fontWeight: 400,
fontSize: "14px",
lineHeight: "17px",
color: theme.palette.grey2.main,
mb: upMd ? "9px" : "20px",
}}
>
<Box
Создан: 15.09.22 08:39
</Typography>
<Box
sx={{
backgroundColor: "#ECECF3",
border: `1px solid ${theme.palette.grey2.main}`,
borderRadius: "10px",
overflow: "hidden",
width: "100%",
minHeight: "345px",
display: "flex",
flexGrow: 1,
flexDirection: "column",
}}
>
<Box
sx={{
position: "relative",
width: "100%",
flexGrow: 1,
borderBottom: `1px solid ${theme.palette.grey2.main}`,
height: "200px",
}}
>
{isPreventAutoscroll && (
<Fab
size="small"
onClick={scrollToBottom}
sx={{
display: "flex",
alignItems: "start",
flexDirection: "column",
flexGrow: 1,
position: "absolute",
left: "10px",
bottom: "10px",
}}
>
<ArrowDownwardIcon />
</Fab>
)}
<Box
ref={chatBoxRef}
sx={{
display: "flex",
width: "100%",
flexDirection: "column",
gap: upMd ? "20px" : "16px",
px: upMd ? "20px" : "5px",
py: upMd ? "20px" : "13px",
overflowY: "auto",
height: "100%",
}}
>
<Typography variant={upMd ? "h5" : "body2"} mb={"4px"}>Заголовок</Typography>
<Typography sx={{
fontWeight: 400,
fontSize: "14px",
lineHeight: "17px",
color: theme.palette.grey2.main,
mb: upMd ? "9px" : "20px",
}}>Создан: 15.09.22 08:39</Typography>
<Box
sx={{
backgroundColor: "#ECECF3",
border: `1px solid ${theme.palette.grey2.main}`,
borderRadius: "10px",
overflow: "hidden",
width: "100%",
minHeight: "345px",
display: "flex",
flexGrow: 1,
flexDirection: "column",
}}
>
<Box sx={{
position: "relative",
width: "100%",
flexGrow: 1,
borderBottom: `1px solid ${theme.palette.grey2.main}`,
height: "200px",
}}>
{isPreventAutoscroll &&
<Fab
size="small"
onClick={scrollToBottom}
sx={{
position: "absolute",
left: "10px",
bottom: "10px",
}}
>
<ArrowDownwardIcon />
</Fab>
}
<Box
ref={chatBoxRef}
sx={{
display: "flex",
width: "100%",
flexDirection: "column",
gap: upMd ? "20px" : "16px",
px: upMd ? "20px" : "5px",
py: upMd ? "20px" : "13px",
overflowY: "auto",
height: "100%",
}}
>
{messages.map(message =>
<Message
key={message.id}
text={message.message}
time={new Date(message.created_at).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}
isSelf={true}
/>
)}
</Box>
</Box>
<FormControl>
<InputBase
value={messageText}
fullWidth
placeholder="Текст обращения"
id="message"
multiline
sx={{
width: "100%",
p: 0,
}}
inputProps={{
sx: {
fontWeight: 400,
fontSize: "16px",
lineHeight: "19px",
pt: upMd ? "13px" : "28px",
pb: upMd ? "13px" : "24px",
px: "19px",
maxHeight: "calc(19px * 5)",
}
}}
onChange={e => setMessageText(e.target.value)}
endAdornment={!upMd &&
<InputAdornment position="end">
<IconButton
onClick={handleSendMessage}
sx={{
height: "45px",
width: "45px",
mr: "13px",
p: 0,
}}
>
<SendIcon />
</IconButton>
</InputAdornment>
}
/>
</FormControl>
</Box>
{messages.map((message) => (
<Message
key={message.id}
text={message.message}
time={new Date(message.created_at).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}
isSelf={true}
/>
))}
</Box>
{upMd &&
<Box sx={{ alignSelf: "end" }}>
<CustomButton
onClick={handleSendMessage}
variant="contained"
sx={{
backgroundColor: theme.palette.brightPurple.main,
}}
>Отправить</CustomButton>
</Box>
}
</Box>
<FormControl>
<InputBase
value={messageText}
fullWidth
placeholder="Текст обращения"
id="message"
multiline
sx={{
width: "100%",
p: 0,
}}
inputProps={{
sx: {
fontWeight: 400,
fontSize: "16px",
lineHeight: "19px",
pt: upMd ? "13px" : "28px",
pb: upMd ? "13px" : "24px",
px: "19px",
maxHeight: "calc(19px * 5)",
},
}}
onChange={(e) => setMessageText(e.target.value)}
endAdornment={
!upMd && (
<InputAdornment position="end">
<IconButton
onClick={handleSendMessage}
sx={{
height: "45px",
width: "45px",
mr: "13px",
p: 0,
}}
>
<SendIcon />
</IconButton>
</InputAdornment>
)
}
/>
</FormControl>
</Box>
);
}
</Box>
{upMd && (
<Box sx={{ alignSelf: "end" }}>
<CustomButton
onClick={handleSendMessage}
variant="contained"
sx={{
backgroundColor: theme.palette.brightPurple.main,
}}
>
Отправить
</CustomButton>
</Box>
)}
</Box>
);
}

@ -1,83 +1,94 @@
import { Box, Typography, useMediaQuery, useTheme } from "@mui/material";
import CustomButton from "../../components/CustomButton";
import { Link as RouterLink } from "react-router-dom";
import { useMemo } from "react";
import { Box, Typography, useMediaQuery, useTheme } from "@mui/material";
import CustomButton from "@components/CustomButton";
interface Props {
name: string;
body: string;
time: string;
ticketId: string;
name: string;
body: string;
time: string;
ticketId: string;
}
export default function TicketCard({ name, body, time, ticketId }: Props) {
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const timeText = useMemo(() => (
<Typography sx={{
fontWeight: 400,
fontSize: "14px",
lineHeight: "17px",
color: theme.palette.grey2.main,
mt: "2px",
mb: "5px",
}}>{time}</Typography>
), [theme.palette.grey2.main, time]);
const timeText = useMemo(
() => (
<Typography
sx={{
fontWeight: 400,
fontSize: "14px",
lineHeight: "17px",
color: theme.palette.grey2.main,
mt: "2px",
mb: "5px",
}}
>
{time}
</Typography>
),
[theme.palette.grey2.main, time]
);
return (
<Box
sx={{
width: "100%",
p: "20px",
backgroundColor: "white",
borderRadius: "12px",
display: "flex",
flexDirection: upMd ? "row" : "column",
justifyContent: "space-between",
gap: upMd ? "40px" : "20px",
boxShadow: `0px 100px 309px rgba(210, 208, 225, 0.24),
return (
<Box
sx={{
width: "100%",
p: "20px",
backgroundColor: "white",
borderRadius: "12px",
display: "flex",
flexDirection: upMd ? "row" : "column",
justifyContent: "space-between",
gap: upMd ? "40px" : "20px",
boxShadow: `0px 100px 309px rgba(210, 208, 225, 0.24),
0px 41.7776px 129.093px rgba(210, 208, 225, 0.172525),
0px 22.3363px 69.0192px rgba(210, 208, 225, 0.143066),
0px 12.5216px 38.6916px rgba(210, 208, 225, 0.12),
0px 6.6501px 20.5488px rgba(210, 208, 225, 0.0969343),
0px 2.76726px 8.55082px rgba(210, 208, 225, 0.0674749)`,
}}
}}
>
{!upMd && timeText}
<Box>
<Typography
sx={{
mb: "20px",
fontSize: "18px",
lineHeight: "21px",
fontWeight: 500,
}}
>
{!upMd && timeText}
<Box>
<Typography
sx={{
mb: "20px",
fontSize: "18px",
lineHeight: "21px",
fontWeight: 500,
}}
>{name}</Typography>
<Typography color={theme.palette.grey3.main}>{body}</Typography>
</Box>
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: upMd ? "end" : "start",
justifyContent: "space-between",
gap: "10px",
}}
>
{upMd && timeText}
<CustomButton
variant="outlined"
component={RouterLink}
to={`/support/${ticketId}`}
sx={{
py: "9px",
color: theme.palette.brightPurple.main,
borderColor: theme.palette.brightPurple.main,
}}
>Перейти</CustomButton>
</Box>
</Box>
);
}
{name}
</Typography>
<Typography color={theme.palette.grey3.main}>{body}</Typography>
</Box>
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: upMd ? "end" : "start",
justifyContent: "space-between",
gap: "10px",
}}
>
{upMd && timeText}
<CustomButton
variant="outlined"
component={RouterLink}
to={`/support/${ticketId}`}
sx={{
py: "9px",
color: theme.palette.brightPurple.main,
borderColor: theme.palette.brightPurple.main,
}}
>
Перейти
</CustomButton>
</Box>
</Box>
);
}

@ -1,120 +1,134 @@
import { CircularProgress, List, ListItem, Box, useTheme, Pagination } from "@mui/material";
import { useEffect, useState } from "react";
import { apiRequestHandler } from "../../utils/api/apiRequestHandler";
import { ApiError, Ticket } from "../../utils/api/types";
import TicketCard from "./TicketCard";
import { useSnackbar } from "notistack";
import { apiRequestHandler } from "@utils/api/apiRequestHandler";
import { ApiError, Ticket } from "@utils/api/types";
const TICKETS_PER_PAGE = 10;
export default function TicketList() {
const theme = useTheme();
const { enqueueSnackbar } = useSnackbar();
const [tickets, setTickets] = useState<Ticket[]>([]);
const [ticketCount, setTicketCount] = useState<number | null>(null);
const [currentPage, setCurrentPage] = useState<number>(0);
const [isLoading, setIsLoading] = useState<boolean>(false);
const theme = useTheme();
const { enqueueSnackbar } = useSnackbar();
const [tickets, setTickets] = useState<Ticket[]>([]);
const [ticketCount, setTicketCount] = useState<number | null>(null);
const [currentPage, setCurrentPage] = useState<number>(0);
const [isLoading, setIsLoading] = useState<boolean>(false);
useEffect(function fetchTickets() {
setIsLoading(true);
const abortController = new AbortController();
useEffect(
function fetchTickets() {
setIsLoading(true);
const abortController = new AbortController();
apiRequestHandler.getTickets({ amt: TICKETS_PER_PAGE, page: currentPage, srch: "", status: "open" }, abortController.signal)
.then(result => {
if (result instanceof ApiError) {
enqueueSnackbar(`Error: ${result.message}`);
} else if (result instanceof Error) {
console.log(result);
} else {
setTickets(result.data);
setTicketCount(result.count);
setIsLoading(false);
}
});
return () => {
abortController.abort();
};
}, [currentPage, enqueueSnackbar]);
useEffect(function subscribeToTickets() {
const unsubscribe = apiRequestHandler.subscribeToAllTickets({
onMessage(event) {
console.log("SSE received:", event.data);
try {
const newTicket = JSON.parse(event.data) as Ticket;
const existingTicketIndex = tickets.findIndex(ticket => ticket.id === newTicket.id);
if (existingTicketIndex !== -1) {
setTickets(prevTickets => {
const newTickets = prevTickets.slice();
newTickets.splice(existingTicketIndex, 1, newTicket);
return newTickets;
});
return;
}
setTickets(prevTickets => [newTicket, ...prevTickets.slice(0, TICKETS_PER_PAGE - 1)]);
} catch (error) {
console.log("SSE is not JSON", error);
}
},
onError(event) {
console.log("SSE Error:", event);
}
apiRequestHandler
.getTickets({ amt: TICKETS_PER_PAGE, page: currentPage, srch: "", status: "open" }, abortController.signal)
.then((result) => {
if (result instanceof ApiError) {
enqueueSnackbar(`Error: ${result.message}`);
} else if (result instanceof Error) {
console.log(result);
} else {
setTickets(result.data);
setTicketCount(result.count);
setIsLoading(false);
}
});
return () => {
unsubscribe();
};
}, [tickets]);
return () => {
abortController.abort();
};
},
[currentPage, enqueueSnackbar]
);
return (
<Box sx={{
display: "flex",
gap: "40px",
flexDirection: "column",
}}>
<List sx={{
p: 0,
display: "flex",
flexDirection: "column",
gap: "40px",
opacity: isLoading ? 0.4 : 1,
transitionProperty: "opacity",
transitionDuration: "200ms",
}}>
{tickets.map(ticket =>
<ListItem key={ticket.id} disablePadding>
<TicketCard
name={ticket.title}
body={ticket.top_message.message}
time={new Date(ticket.updated_at).toLocaleDateString()}
ticketId={ticket.id}
/>
</ListItem>
)}
{isLoading &&
<Box sx={{
position: "absolute",
width: "100%",
height: "100%",
minHeight: "80px",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}>
<CircularProgress sx={{ color: theme.palette.brightPurple.main }} size={60} />
</Box>
}
</List>
{ticketCount !== null && ticketCount > TICKETS_PER_PAGE &&
<Pagination
count={Math.ceil(ticketCount / TICKETS_PER_PAGE)}
page={currentPage + 1}
onChange={(e, value) => setCurrentPage(value - 1)}
sx={{ alignSelf: "center" }}
/>
useEffect(
function subscribeToTickets() {
const unsubscribe = apiRequestHandler.subscribeToAllTickets({
onMessage(event) {
console.log("SSE received:", event.data);
try {
const newTicket = JSON.parse(event.data) as Ticket;
const existingTicketIndex = tickets.findIndex((ticket) => ticket.id === newTicket.id);
if (existingTicketIndex !== -1) {
setTickets((prevTickets) => {
const newTickets = prevTickets.slice();
newTickets.splice(existingTicketIndex, 1, newTicket);
return newTickets;
});
return;
}
</Box>
);
}
setTickets((prevTickets) => [newTicket, ...prevTickets.slice(0, TICKETS_PER_PAGE - 1)]);
} catch (error) {
console.log("SSE is not JSON", error);
}
},
onError(event) {
console.log("SSE Error:", event);
},
});
return () => {
unsubscribe();
};
},
[tickets]
);
return (
<Box
sx={{
display: "flex",
gap: "40px",
flexDirection: "column",
}}
>
<List
sx={{
p: 0,
display: "flex",
flexDirection: "column",
gap: "40px",
opacity: isLoading ? 0.4 : 1,
transitionProperty: "opacity",
transitionDuration: "200ms",
}}
>
{tickets.map((ticket) => (
<ListItem key={ticket.id} disablePadding>
<TicketCard
name={ticket.title}
body={ticket.top_message.message}
time={new Date(ticket.updated_at).toLocaleDateString()}
ticketId={ticket.id}
/>
</ListItem>
))}
{isLoading && (
<Box
sx={{
position: "absolute",
width: "100%",
height: "100%",
minHeight: "80px",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<CircularProgress sx={{ color: theme.palette.brightPurple.main }} size={60} />
</Box>
)}
</List>
{ticketCount !== null && ticketCount > TICKETS_PER_PAGE && (
<Pagination
count={Math.ceil(ticketCount / TICKETS_PER_PAGE)}
page={currentPage + 1}
onChange={(e, value) => setCurrentPage(value - 1)}
sx={{ alignSelf: "center" }}
/>
)}
</Box>
);
}

@ -2,7 +2,7 @@ import { Box, SxProps, Theme } from "@mui/material";
import Typography from "@mui/material/Typography";
import { useNavigate } from "react-router-dom";
import CustomButton from "../../components/CustomButton";
import CustomButton from "@components/CustomButton";
import { IconsCreate } from "../../lib/IconsCreate";

@ -1,7 +1,8 @@
import { Box, SxProps, Theme, useTheme } from "@mui/material";
import { useNavigate } from "react-router-dom";
import Typography from "@mui/material/Typography";
import CustomButton from "../../components/CustomButton";
import CustomButton from "@components/CustomButton";
interface Props {
icon: React.ReactNode;

@ -1,7 +1,8 @@
import { Box, SxProps, Theme, useTheme } from "@mui/material";
import { useNavigate } from "react-router-dom";
import Typography from "@mui/material/Typography";
import CustomButton from "../../components/CustomButton";
import CustomButton from "@components/CustomButton";
interface Props {
icon: React.ReactNode;

@ -1,10 +1,11 @@
import { useMediaQuery, useTheme, Box, Link, Typography } from "@mui/material";
import { Outlet, Route, Routes } from "react-router-dom";
import CardWithLink from "../../components/CardWithLink";
import SectionWrapper from "../../components/SectionWrapper";
import CustomIcon from "../../components/icons/CustomIcon";
import CardWithLink from "@components/CardWithLink";
import SectionWrapper from "@components/SectionWrapper";
import CustomIcon from "@components/icons/CustomIcon";
import TariffCard from "./TariffCard";
import video1 from "../../assets/animations/Icon_1.webm";
import video2 from "../../assets/animations/Icon_2.webm";
import video3 from "../../assets/animations/Icon_3.webm";
@ -91,37 +92,37 @@ export default function Tariffs() {
}}
>
<CardWithLink
shadowType="light"
buttonType="button"
height="520px"
width={upMd ? "31%" : "100%"}
headerText="Шаблонизатор"
text="Текст-заполнитель — это текст, который имеет некоторые характеристики реального письменного текста, но является "
isHighlighted
linkHref="#"
video={video1}
shadowType="light"
buttonType="button"
height="520px"
width={upMd ? "31%" : "100%"}
headerText="Шаблонизатор"
text="Текст-заполнитель — это текст, который имеет некоторые характеристики реального письменного текста, но является "
isHighlighted
linkHref="#"
video={video1}
/>
<CardWithLink
shadowType="light"
buttonType="button"
height="520px"
width={upMd ? "31%" : "100%"}
headerText="Опросник"
text="Текст-заполнитель — это текст, который имеет некоторые характеристики реального письменного текста, но является "
isHighlighted
linkHref="#"
video={video2}
shadowType="light"
buttonType="button"
height="520px"
width={upMd ? "31%" : "100%"}
headerText="Опросник"
text="Текст-заполнитель — это текст, который имеет некоторые характеристики реального письменного текста, но является "
isHighlighted
linkHref="#"
video={video2}
/>
<CardWithLink
shadowType="light"
buttonType="button"
height="520px"
width={upMd ? "31%" : "100%"}
headerText="Сокращатель ссылок"
text="Текст-заполнитель — это текст, который имеет некоторые характеристики реального письменного текста, но является "
isHighlighted
linkHref="#"
video={video3}
shadowType="light"
buttonType="button"
height="520px"
width={upMd ? "31%" : "100%"}
headerText="Сокращатель ссылок"
text="Текст-заполнитель — это текст, который имеет некоторые характеристики реального письменного текста, но является "
isHighlighted
linkHref="#"
video={video3}
/>
</Box>
</>

@ -1,6 +1,6 @@
import { useMediaQuery, useTheme, Box, Typography, List, ListItem } from "@mui/material";
import SectionWrapper from "../../components/SectionWrapper";
import SectionWrapper from "@components/SectionWrapper";
import { TariffCardTimeAndVolume } from "./TariffCardTimeAndVolume";
import { FreeTariffCard } from "./FreeTariffCard";

@ -1,6 +1,6 @@
import { useMediaQuery, useTheme, Box, Typography, List, ListItem } from "@mui/material";
import SectionWrapper from "../../components/SectionWrapper";
import SectionWrapper from "@components/SectionWrapper";
import { TariffCardTimeAndVolume } from "./TariffCardTimeAndVolume";
import { FreeTariffCard } from "./FreeTariffCard";

@ -1,158 +1,148 @@
import { Box, IconButton, Typography, useMediaQuery, useTheme } from "@mui/material";
import CustomButton from "../components/CustomButton";
import WalletIcon from "../components/icons/WalletIcon";
import SectionWrapper from "../components/SectionWrapper";
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import ComplexNavText from "../components/ComplexNavText";
import CustomButton from "@components/CustomButton";
import WalletIcon from "@components/icons/WalletIcon";
import SectionWrapper from "@components/SectionWrapper";
import ComplexNavText from "@components/ComplexNavText";
export default function Wallet() {
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const footnotes = (
<Box
component="ol"
sx={{
color: theme.palette.grey2.main,
pt: "10px",
pl: "25px",
mt: 0,
mb: upMd ? "3px" : "73px",
}}
>
<Typography
component="li"
sx={{
fontSize: "16px",
lineHeight: "20px",
fontWeight: 400,
}}
>
Текст для сносок: текст-заполнитель
это текст, который имеет текст-заполнитель
это текст, который имеет
</Typography>
<Typography
component="li"
sx={{
fontSize: "16px",
lineHeight: "20px",
fontWeight: 400,
}}
>
Текст для сносок: тель
это текст, который имеет текст-заполнитель
это текст, который имеет
</Typography>
</Box>
);
const footnotes = (
<Box
component="ol"
sx={{
color: theme.palette.grey2.main,
pt: "10px",
pl: "25px",
mt: 0,
mb: upMd ? "3px" : "73px",
}}
>
<Typography
component="li"
sx={{
fontSize: "16px",
lineHeight: "20px",
fontWeight: 400,
}}
>
Текст для сносок: текст-заполнитель это текст, который имеет текст-заполнитель это текст, который имеет
</Typography>
<Typography
component="li"
sx={{
fontSize: "16px",
lineHeight: "20px",
fontWeight: 400,
}}
>
Текст для сносок: тель это текст, который имеет текст-заполнитель это текст, который имеет
</Typography>
</Box>
);
return (
<SectionWrapper
maxWidth="lg"
sx={{
mt: "25px",
mb: "70px",
}}
>
{upMd &&
<ComplexNavText text1="Все тарифы — " text2="Мой кошелёк" />
}
<Box
sx={{
mt: "20px",
mb: "40px",
display: "flex",
gap: "10px",
}}
>
{!upMd &&
<IconButton sx={{ p: 0, height: "28px", width: "28px", color: "black" }}>
<ArrowBackIcon />
</IconButton>
}
<Typography variant="h4">Мой кошелёк</Typography>
</Box>
<Box
sx={{
backgroundColor: "white",
display: "flex",
flexDirection: upMd ? "row" : "column",
gap: "9%",
borderRadius: "12px",
mb: "40px",
boxShadow: `0px 100px 309px rgba(210, 208, 225, 0.24),
return (
<SectionWrapper
maxWidth="lg"
sx={{
mt: "25px",
mb: "70px",
}}
>
{upMd && <ComplexNavText text1="Все тарифы — " text2="Мой кошелёк" />}
<Box
sx={{
mt: "20px",
mb: "40px",
display: "flex",
gap: "10px",
}}
>
{!upMd && (
<IconButton sx={{ p: 0, height: "28px", width: "28px", color: "black" }}>
<ArrowBackIcon />
</IconButton>
)}
<Typography variant="h4">Мой кошелёк</Typography>
</Box>
<Box
sx={{
backgroundColor: "white",
display: "flex",
flexDirection: upMd ? "row" : "column",
gap: "9%",
borderRadius: "12px",
mb: "40px",
boxShadow: `0px 100px 309px rgba(210, 208, 225, 0.24),
0px 41.7776px 129.093px rgba(210, 208, 225, 0.172525),
0px 22.3363px 69.0192px rgba(210, 208, 225, 0.143066),
0px 12.5216px 38.6916px rgba(210, 208, 225, 0.12),
0px 6.6501px 20.5488px rgba(210, 208, 225, 0.0969343),
0px 2.76726px 8.55082px rgba(210, 208, 225, 0.0674749)`,
}}
>
<Box
sx={{
width: upMd ? "59.5%" : undefined,
p: "20px",
pb: upMd ? undefined : "10px",
}}
>
<Typography sx={{ color: theme.palette.grey3.main, mb: "30px" }}>Баланс 10.04.2022</Typography>
<Box
sx={{
display: "flex",
alignItems: "center",
gap: "22px",
pb: "40px",
borderBottom: `1px solid ${theme.palette.grey2.main}`,
}}
>
<WalletIcon bgcolor="#FEDFD0" color={theme.palette.orange.main} />
<Typography variant="h5">10 304 руб.</Typography>
</Box>
{upMd && footnotes}
</Box>
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "start",
color: theme.palette.grey3.main,
width: upMd ? "31.5%" : undefined,
p: "20px",
pl: upMd ? "33px" : undefined,
borderLeft: upMd ? `1px solid ${theme.palette.grey2.main}` : undefined,
}}
>
<Box
sx={{
display: "flex",
flexDirection: "column",
gap: "22px",
maxWidth: "85%",
mb: "32px",
}}
>
<Typography>
Текст-заполнитель
это текст, который имеет
</Typography>
<Typography>
Текст-заполнитель
это текст, который имеет
</Typography>
</Box>
<CustomButton
variant="contained"
sx={{
backgroundColor: theme.palette.brightPurple.main,
textColor: "white",
mt: "auto",
}}
>Пополнить</CustomButton>
</Box>
</Box>
{!upMd && footnotes}
</SectionWrapper>
);
}
}}
>
<Box
sx={{
width: upMd ? "59.5%" : undefined,
p: "20px",
pb: upMd ? undefined : "10px",
}}
>
<Typography sx={{ color: theme.palette.grey3.main, mb: "30px" }}>Баланс 10.04.2022</Typography>
<Box
sx={{
display: "flex",
alignItems: "center",
gap: "22px",
pb: "40px",
borderBottom: `1px solid ${theme.palette.grey2.main}`,
}}
>
<WalletIcon bgcolor="#FEDFD0" color={theme.palette.orange.main} />
<Typography variant="h5">10 304 руб.</Typography>
</Box>
{upMd && footnotes}
</Box>
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "start",
color: theme.palette.grey3.main,
width: upMd ? "31.5%" : undefined,
p: "20px",
pl: upMd ? "33px" : undefined,
borderLeft: upMd ? `1px solid ${theme.palette.grey2.main}` : undefined,
}}
>
<Box
sx={{
display: "flex",
flexDirection: "column",
gap: "22px",
maxWidth: "85%",
mb: "32px",
}}
>
<Typography>Текст-заполнитель это текст, который имеет</Typography>
<Typography>Текст-заполнитель это текст, который имеет</Typography>
</Box>
<CustomButton
variant="contained"
sx={{
backgroundColor: theme.palette.brightPurple.main,
textColor: "white",
mt: "auto",
}}
>
Пополнить
</CustomButton>
</Box>
</Box>
{!upMd && footnotes}
</SectionWrapper>
);
}

@ -1,10 +1,10 @@
{
"compilerOptions": {
"baseUrl": "./src",
"paths": {
"@root/*": ["./*"],
"@utils/*": ["./utils/*"],
"@components/*": ["./utils/components/*"]
}
"compilerOptions": {
"baseUrl": "./src",
"paths": {
"@root/*": ["./*"],
"@utils/*": ["./utils/*"],
"@components/*": ["./components/*"]
}
}
}
}