This commit is contained in:
Nastya 2023-08-07 15:57:44 +00:00
parent 1df8502813
commit 24399db30b
24 changed files with 1670 additions and 788 deletions

11
cypress.config.ts Normal file

@ -0,0 +1,11 @@
import { defineConfig } from "cypress";
export default defineConfig({
e2e: {
viewportWidth: 1200,
viewportHeight: 800,
fixturesFolder: "tests/e2e/fixtures",
supportFile: false,
defaultCommandTimeout: 100,
},
});

125
cypress/e2e/access.cy.ts Normal file

@ -0,0 +1,125 @@
describe("Форма Входа", () => {
beforeEach(() => {
cy.visit("http://localhost:3000");
cy.wait(1000);
cy.contains("Личный кабинет").click();
});
it("должна успешно входить с правильными учетными данными", () => {
const login = "valid_user@example.com";
const password = "valid_password";
cy.get("#login").type(login);
cy.get("#password").type(password);
cy.get('button[type="submit"]').click();
cy.wait(2000);
cy.url().should("include", "http://localhost:3000/tariffs");
});
it("должна отображать два сообщение об ошибке при отсутствии полей", () => {
cy.get('button[type="submit"]').click();
cy.wait(2000);
cy.get("#password-helper-text").should("contain", "Поле обязательно");
cy.get("#login-helper-text").should("contain", "Поле обязательно");
});
it("должна отображать сообщение об ошибке при отсутствии пароля", () => {
cy.get("#login").type("valid_email@example.com");
cy.get('button[type="submit"]').click();
cy.get("#password-helper-text").should("contain", "Поле обязательно");
});
it("должна отображать сообщение об ошибке при отсутствии Логина", () => {
cy.get("#password").type("valid_password");
cy.get('button[type="submit"]').click();
cy.get("#login-helper-text").should("contain", "Поле обязательно");
});
});
describe("Форма регистрации", () => {
beforeEach(() => {
cy.visit("http://localhost:3000");
cy.wait(1000);
cy.contains("Личный кабинет").click();
cy.contains("Регистрация").click();
});
it("должна регистрировать нового пользователя с правильными данными", () => {
const login = Cypress._.random(1000) + "@example.com";
const password = "valid_password";
cy.get("#login").type(login);
cy.get("#password").type(password);
cy.get("#repeatPassword").type(password);
cy.get('button[type="submit"]').click();
cy.wait(5000);
cy.url().should("include", "http://localhost:3000/tariffs");
});
it("должна отображать ошибку при отсутсвии логина", () => {
cy.get("#password").type("valid_password");
cy.get("#repeatPassword").type("valid_password");
cy.get('button[type="submit"]').click();
cy.get("#login-helper-text").should("contain", "Поле обязательно");
});
it("должна отображать ошибку при отсутствии пароля", () => {
cy.get("#login").type("valid_login");
cy.get('button[type="submit"]').click();
cy.get("#password-helper-text").should("contain", "Поле обязательно");
});
it("должна отображать ошибку при отсутствии поля Повторения пароля", () => {
cy.get("#login").type("valid_login");
cy.get("#password").type("valid_password");
cy.get('button[type="submit"]').click();
cy.get("#repeatPassword-helper-text").should("contain", "Повторите пароль");
});
it("должна отображать ошибку при некоректном пароле", () => {
cy.get("#login").type("valid_log");
cy.get("#password").type("valid@12_-_@@password");
cy.get('button[type="submit"]').click();
cy.get("#password-helper-text").should("contain", "Некорректные символы");
cy.get("#repeatPassword-helper-text").should("contain", "Повторите пароль");
});
it("должна отображать ошибку при несовпадении паролей", () => {
cy.get("#login").type("valid_login");
cy.get("#password").type("valid_password");
cy.get("#repeatPassword").type("invalidPassword");
cy.get('button[type="submit"]').click();
cy.get("#repeatPassword-helper-text").should("contain", "Пароли не совпадают");
});
it("попытка отправки запроса при уже зарегистрированном пользователе", () => {
const login = "valid_user@example.com";
const password = "valid_password";
cy.get("#login").type(login);
cy.get("#password").type(password);
cy.get("#repeatPassword").type(password);
cy.get('button[type="submit"]').click();
cy.wait(5000);
cy.contains("user with this login is exist");
});
});

@ -1,70 +1,73 @@
{ {
"name": "hub_frontend", "name": "hub_frontend",
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"scripts": { "scripts": {
"start": "craco start", "start": "craco start",
"build": "craco build", "build": "craco build",
"test": "craco test --env=node --transformIgnorePatterns \"node_modules/(?!@frontend)/\"", "test": "craco test --env=node --transformIgnorePatterns \"node_modules/(?!@frontend)/\"",
"test:cart": "craco test src/utils/calcCart --transformIgnorePatterns \"node_modules/(?!@frontend)/\"", "test:cart": "craco test src/utils/calcCart --transformIgnorePatterns \"node_modules/(?!@frontend)/\"",
"eject": "craco eject" "eject": "craco eject",
}, "test:cypress": "start-server-and-test start http://localhost:3000 cypress",
"dependencies": { "cypress": "cypress open"
"@emotion/react": "^11.10.5", },
"@emotion/styled": "^11.10.5", "dependencies": {
"@frontend/kitui": "^1.0.17", "@emotion/react": "^11.10.5",
"@mui/icons-material": "^5.10.14", "@emotion/styled": "^11.10.5",
"@mui/material": "^5.10.14", "@frontend/kitui": "^1.0.17",
"@popperjs/core": "^2.11.8", "@mui/icons-material": "^5.10.14",
"axios": "^1.4.0", "@mui/material": "^5.10.14",
"buffer": "^6.0.3", "@popperjs/core": "^2.11.8",
"classnames": "^2.3.2", "axios": "^1.4.0",
"formik": "^2.2.9", "buffer": "^6.0.3",
"immer": "^10.0.2", "classnames": "^2.3.2",
"isomorphic-fetch": "^3.0.0", "cypress": "^12.17.3",
"notistack": "^3.0.1", "formik": "^2.2.9",
"pdfjs-dist": "3.6.172", "immer": "^10.0.2",
"react": "^18.2.0", "isomorphic-fetch": "^3.0.0",
"react-dom": "^18.2.0", "notistack": "^3.0.1",
"react-pdf": "^7.1.2", "pdfjs-dist": "3.6.172",
"react-router-dom": "^6.4.3", "react": "^18.2.0",
"react-slick": "^0.29.0", "react-dom": "^18.2.0",
"slick-carousel": "^1.8.1", "react-pdf": "^7.1.2",
"web-vitals": "^2.1.0", "react-router-dom": "^6.4.3",
"yup": "^1.1.1", "react-slick": "^0.29.0",
"zustand": "^4.3.8" "slick-carousel": "^1.8.1",
}, "web-vitals": "^2.1.0",
"devDependencies": { "yup": "^1.1.1",
"@craco/craco": "^7.1.0", "zustand": "^4.3.8"
"@testing-library/jest-dom": "^5.14.1", },
"@testing-library/react": "^13.0.0", "devDependencies": {
"@testing-library/user-event": "^13.2.1", "@craco/craco": "^7.1.0",
"@types/jest": "^27.0.1", "@testing-library/jest-dom": "^5.14.1",
"@types/node": "^16.7.13", "@testing-library/react": "^13.0.0",
"@types/react": "^18.0.0", "@testing-library/user-event": "^13.2.1",
"@types/react-dom": "^18.0.0", "@types/jest": "^27.0.1",
"@types/react-slick": "^0.23.10", "@types/node": "^16.7.13",
"craco-alias": "^3.0.1", "@types/react": "^18.0.0",
"jest": "^29.5.0", "@types/react-dom": "^18.0.0",
"react-scripts": "5.0.1", "@types/react-slick": "^0.23.10",
"typescript": "^4.9.3" "craco-alias": "^3.0.1",
}, "jest": "^29.5.0",
"eslintConfig": { "react-scripts": "5.0.1",
"extends": [ "typescript": "^4.9.3"
"react-app", },
"react-app/jest" "eslintConfig": {
] "extends": [
}, "react-app",
"browserslist": { "react-app/jest"
"production": [ ]
">0.2%", },
"not dead", "browserslist": {
"not op_mini all" "production": [
], ">0.2%",
"development": [ "not dead",
"last 1 chrome version", "not op_mini all"
"last 1 firefox version", ],
"last 1 safari version" "development": [
] "last 1 chrome version",
} "last 1 firefox version",
"last 1 safari version"
]
}
} }

@ -51,9 +51,7 @@ export default function Drawers() {
(state) => state.summaryPriceAfterDiscountsMap (state) => state.summaryPriceAfterDiscountsMap
); );
const userAccount = useUserStore((state) => state.userAccount); const userAccount = useUserStore((state) => state.userAccount);
const { tickets, ticketCount, apiPage, ticketsPerPage } = useTicketStore( const { tickets, apiPage, ticketsPerPage } = useTicketStore((state) => state);
(state) => state
);
useTickets({ useTickets({
url: "https://hub.pena.digital/heruvym/getTickets", url: "https://hub.pena.digital/heruvym/getTickets",
@ -75,6 +73,10 @@ export default function Drawers() {
0 0
); );
const notificationsCount = tickets.filter(
({ user, top_message }) => user !== top_message.user_id
).length;
const totalPriceBeforeDiscounts = cart.priceBeforeDiscounts + basePrice; const totalPriceBeforeDiscounts = cart.priceBeforeDiscounts + basePrice;
const totalPriceAfterDiscounts = cart.priceAfterDiscounts + discountedPrice; const totalPriceAfterDiscounts = cart.priceAfterDiscounts + discountedPrice;
@ -119,10 +121,10 @@ export default function Drawers() {
}} }}
> >
<Badge <Badge
badgeContent={ticketCount} badgeContent={notificationsCount}
sx={{ sx={{
"& .MuiBadge-badge": { "& .MuiBadge-badge": {
display: ticketCount ? "flex" : "none", display: notificationsCount ? "flex" : "none",
color: "#FFFFFF", color: "#FFFFFF",
background: theme.palette.brightPurple.main, background: theme.palette.brightPurple.main,
transform: "scale(0.8) translate(50%, -50%)", transform: "scale(0.8) translate(50%, -50%)",
@ -136,12 +138,13 @@ export default function Drawers() {
</Badge> </Badge>
</IconButton> </IconButton>
<NotificationsModal <NotificationsModal
open={openNotificationsModal} open={notificationsCount ? openNotificationsModal : false}
setOpen={setOpenNotificationsModal} setOpen={setOpenNotificationsModal}
anchorElement={bellRef.current} anchorElement={bellRef.current}
notifications={tickets.map((ticket) => ({ notifications={tickets.map((ticket) => ({
text: "У вас новое сообщение от техподдержки", text: "У вас новое сообщение от техподдержки",
date: new Date(ticket.updated_at).toLocaleDateString(), date: new Date(ticket.updated_at).toLocaleDateString(),
url: `/support/${ticket.id}`,
watched: ticket.user === ticket.top_message.user_id, watched: ticket.user === ticket.top_message.user_id,
}))} }))}
/> />

@ -12,6 +12,7 @@ import {
import { useUserStore } from "@root/stores/user"; import { useUserStore } from "@root/stores/user";
import { currencyFormatter } from "@root/utils/currencyFormatter"; import { currencyFormatter } from "@root/utils/currencyFormatter";
import { cardShadow } from "@root/utils/themes/shadow";
import CustomAvatar from "./Avatar"; import CustomAvatar from "./Avatar";
@ -45,6 +46,7 @@ export default function DialogMenu({ handleClose }: DialogMenuProps) {
const [activeSubMenuIndex, setActiveSubMenuIndex] = useState<number>(-1); const [activeSubMenuIndex, setActiveSubMenuIndex] = useState<number>(-1);
const theme = useTheme(); const theme = useTheme();
const location = useLocation(); const location = useLocation();
const isTablet = useMediaQuery(theme.breakpoints.down(900));
const isMobile = useMediaQuery(theme.breakpoints.down(600)); const isMobile = useMediaQuery(theme.breakpoints.down(600));
const user = useUserStore((state) => state.user); const user = useUserStore((state) => state.user);
const cash = useUserStore((state) => state.userAccount?.wallet.cash) ?? 0; const cash = useUserStore((state) => state.userAccount?.wallet.cash) ?? 0;
@ -118,12 +120,13 @@ export default function DialogMenu({ handleClose }: DialogMenuProps) {
sx={{ sx={{
backgroundColor: theme.palette.background.paper, backgroundColor: theme.palette.background.paper,
width: "100%", width: "100%",
boxShadow: !isTablet ? cardShadow : null,
}} }}
> >
{index === activeSubMenuIndex && {index === activeSubMenuIndex &&
subMenu.map(({ name, url }) => ( subMenu.map(({ name, url }) => (
<Link <Link
key={url} key={name + url}
style={{ style={{
paddingLeft: "30px", paddingLeft: "30px",
display: "block", display: "block",

@ -1,4 +1,4 @@
import { useMediaQuery, useTheme } from "@mui/material"; import { Box, useMediaQuery, useTheme } from "@mui/material";
import NavbarCollapsed from "./NavbarCollapsed"; import NavbarCollapsed from "./NavbarCollapsed";
import NavbarFull from "./NavbarFull"; import NavbarFull from "./NavbarFull";
@ -14,12 +14,17 @@ export default function Navbar({ isLoggedIn, children }: Props) {
const upMd = useMediaQuery(theme.breakpoints.up("md")); const upMd = useMediaQuery(theme.breakpoints.up("md"));
return ( return (
<> <Box
sx={{
paddingTop: upMd ? "80px" : 0,
width: "100%",
}}
>
{upMd ? ( {upMd ? (
<NavbarFull isLoggedIn={isLoggedIn}>{children}</NavbarFull> <NavbarFull isLoggedIn={isLoggedIn}>{children}</NavbarFull>
) : ( ) : (
<NavbarCollapsed isLoggedIn={isLoggedIn}>{children}</NavbarCollapsed> <NavbarCollapsed isLoggedIn={isLoggedIn}>{children}</NavbarCollapsed>
)} )}
</> </Box>
); );
} }

@ -34,9 +34,7 @@ export default function NavbarCollapsed({ isLoggedIn, children }: Props) {
useState<boolean>(false); useState<boolean>(false);
const bellRef = useRef<HTMLButtonElement | null>(null); const bellRef = useRef<HTMLButtonElement | null>(null);
const userAccount = useUserStore((state) => state.userAccount); const userAccount = useUserStore((state) => state.userAccount);
const { ticketCount, tickets, apiPage, ticketsPerPage } = useTicketStore( const { tickets, apiPage, ticketsPerPage } = useTicketStore((state) => state);
(state) => state
);
useTickets({ useTickets({
url: "https://hub.pena.digital/heruvym/getTickets", url: "https://hub.pena.digital/heruvym/getTickets",
@ -55,6 +53,10 @@ export default function NavbarCollapsed({ isLoggedIn, children }: Props) {
setOpen(false); setOpen(false);
}; };
const notificationsCount = tickets.filter(
({ user, top_message }) => user !== top_message.user_id
).length;
useEffect(() => { useEffect(() => {
if (open) { if (open) {
document.body.style.overflow = "hidden"; document.body.style.overflow = "hidden";
@ -71,36 +73,80 @@ export default function NavbarCollapsed({ isLoggedIn, children }: Props) {
maxWidth="lg" maxWidth="lg"
outerContainerSx={{ outerContainerSx={{
backgroundColor: theme.palette.navbarbg.main, backgroundColor: theme.palette.navbarbg.main,
position: "sticky",
top: 0,
}} }}
sx={{ height: "51px", padding: "0" }} sx={{ height: "51px", padding: "0" }}
> >
<Box <Box sx={{ height: "100%" }}>
sx={{ <Box
display: "flex",
columnGap: "10px",
alignItems: "center",
height: "100%",
padding: "0 18px",
}}
>
<IconButton
onClick={() => setOpen((isOpened) => !isOpened)}
sx={{ sx={{
p: 0, zIndex: 2,
width: "30px", position: "fixed",
color: theme.palette.primary.main, top: 0,
left: 0,
width: "100%",
display: "flex",
columnGap: "10px",
alignItems: "center",
height: "51px",
padding: "0 18px",
background: "#FFFFFF",
}} }}
> >
{open ? (
<CloseIcon />
) : (
<MenuIcon sx={{ height: "30px", width: "30px" }} />
)}
</IconButton>
<Link to="/cart">
<IconButton <IconButton
onClick={() => setOpen((isOpened) => !isOpened)}
sx={{
p: 0,
width: "30px",
color: theme.palette.primary.main,
}}
>
{open ? (
<CloseIcon />
) : (
<MenuIcon sx={{ height: "30px", width: "30px" }} />
)}
</IconButton>
<Link to="/cart">
<IconButton
aria-label="cart"
sx={{
width: "30px",
height: "30px",
background: theme.palette.background.default,
borderRadius: "6px",
"&:hover": {
background: theme.palette.brightPurple.main,
"& .MuiBadge-badge": {
background: theme.palette.background.default,
color: theme.palette.brightPurple.main,
},
"& svg > path:nth-child(1)": { fill: "#FFFFFF" },
"& svg > path:nth-child(2)": { fill: "#FFFFFF" },
"& svg > path:nth-child(3)": { stroke: "#FFFFFF" },
},
}}
>
<Badge
badgeContent={userAccount?.cart.length}
sx={{
"& .MuiBadge-badge": {
display: userAccount?.cart.length ? "flex" : "none",
color: "#FFFFFF",
background: theme.palette.brightPurple.main,
transform: "scale(0.7) translate(50%, -50%)",
top: "2px",
right: "3px",
fontWeight: 400,
},
}}
>
<CartIcon />
</Badge>
</IconButton>
</Link>
<IconButton
ref={bellRef}
onClick={() => setOpenNotificationsModal((isOpened) => !isOpened)}
aria-label="cart" aria-label="cart"
sx={{ sx={{
width: "30px", width: "30px",
@ -113,80 +159,43 @@ export default function NavbarCollapsed({ isLoggedIn, children }: Props) {
background: theme.palette.background.default, background: theme.palette.background.default,
color: theme.palette.brightPurple.main, color: theme.palette.brightPurple.main,
}, },
"& svg > path:nth-child(1)": { fill: "#FFFFFF" }, "& svg > path:first-child": { fill: "#FFFFFF" },
"& svg > path:nth-child(2)": { fill: "#FFFFFF" }, "& svg > path:last-child": { stroke: "#FFFFFF" },
"& svg > path:nth-child(3)": { stroke: "#FFFFFF" },
}, },
}} }}
> >
<Badge <Badge
badgeContent={userAccount?.cart.length} badgeContent={notificationsCount}
sx={{ sx={{
"& .MuiBadge-badge": { "& .MuiBadge-badge": {
display: userAccount?.cart.length ? "flex" : "none", display: notificationsCount ? "flex" : "none",
color: "#FFFFFF", color: "#FFFFFF",
background: theme.palette.brightPurple.main, background: theme.palette.brightPurple.main,
transform: "scale(0.7) translate(50%, -50%)", transform: "scale(0.7) translate(50%, -50%)",
top: "2px", top: "3px",
right: "3px", right: "3px",
fontWeight: 400, fontWeight: 400,
}, },
}} }}
> >
<CartIcon /> <BellIcon />
</Badge> </Badge>
</IconButton> </IconButton>
</Link> <NotificationsModal
<IconButton open={notificationsCount ? openNotificationsModal : false}
ref={bellRef} setOpen={setOpenNotificationsModal}
onClick={() => setOpenNotificationsModal((isOpened) => !isOpened)} anchorElement={bellRef.current}
aria-label="cart" notifications={tickets.map((ticket) => ({
sx={{ text: "У вас новое сообщение от техподдержки",
width: "30px", date: new Date(ticket.updated_at).toLocaleDateString(),
height: "30px", url: `/support/${ticket.id}`,
background: theme.palette.background.default, watched: ticket.user === ticket.top_message.user_id,
borderRadius: "6px", }))}
"&:hover": { />
background: theme.palette.brightPurple.main, <Link to="/" style={{ marginLeft: "auto" }}>
"& .MuiBadge-badge": { <PenaLogo width={100} />
background: theme.palette.background.default, </Link>
color: theme.palette.brightPurple.main, </Box>
},
"& svg > path:first-child": { fill: "#FFFFFF" },
"& svg > path:last-child": { stroke: "#FFFFFF" },
},
}}
>
<Badge
badgeContent={ticketCount}
sx={{
"& .MuiBadge-badge": {
display: ticketCount ? "flex" : "none",
color: "#FFFFFF",
background: theme.palette.brightPurple.main,
transform: "scale(0.7) translate(50%, -50%)",
top: "3px",
right: "3px",
fontWeight: 400,
},
}}
>
<BellIcon />
</Badge>
</IconButton>
<NotificationsModal
open={openNotificationsModal}
setOpen={setOpenNotificationsModal}
anchorElement={bellRef.current}
notifications={tickets.map((ticket) => ({
text: "У вас новое сообщение от техподдержки",
date: new Date(ticket.updated_at).toLocaleDateString(),
watched: ticket.user === ticket.top_message.user_id,
}))}
/>
<Link to="/" style={{ marginLeft: "auto" }}>
<PenaLogo width={100} />
</Link>
</Box> </Box>
<Box sx={{ display: "flex", overflow: open ? "hidden" : "unset" }}> <Box sx={{ display: "flex", overflow: open ? "hidden" : "unset" }}>
<Drawer <Drawer
@ -195,10 +204,11 @@ export default function NavbarCollapsed({ isLoggedIn, children }: Props) {
position: "relative", position: "relative",
zIndex: open ? "none" : "-1", zIndex: open ? "none" : "-1",
"& .MuiDrawer-paper": { "& .MuiDrawer-paper": {
position: "absolute", position: "fixed",
top: "0", top: "0",
width: 210, width: 210,
height: "100%", height: "100%",
marginTop: "51px",
}, },
}} }}
variant="persistent" variant="persistent"

@ -58,6 +58,8 @@ export default function NavbarFull({ isLoggedIn, children }: Props) {
disableGutters disableGutters
maxWidth={false} maxWidth={false}
sx={{ sx={{
position: "fixed",
top: "0",
px: "16px", px: "16px",
display: "flex", display: "flex",
height: "80px", height: "80px",

@ -3,209 +3,215 @@ import { TransitionProps } from "@mui/material/transitions";
import logotip from "../../assets/Icons/logoPenaHab.svg"; import logotip from "../../assets/Icons/logoPenaHab.svg";
import logotipBlack from "../../assets/Icons/black_logo_PenaHab.svg"; import logotipBlack from "../../assets/Icons/black_logo_PenaHab.svg";
import CustomAvatar from "./Avatar"; import CustomAvatar from "./Avatar";
import CloseIcon from "../icons/CloseIcons";
import React from "react"; import React from "react";
import { import {
AppBar, Box,
Box, Button,
Button, Dialog,
Dialog, List,
IconButton, ListItem,
List, Slide,
ListItem, Typography,
Slide, useMediaQuery,
Toolbar, useTheme,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material"; } from "@mui/material";
import { Link, useLocation } from "react-router-dom"; import { Link, useLocation } from "react-router-dom";
import { useUserStore } from "@root/stores/user"; import { useUserStore } from "@root/stores/user";
import { currencyFormatter } from "@root/utils/currencyFormatter"; import { currencyFormatter } from "@root/utils/currencyFormatter";
const arrayMenu = [ const arrayMenu = [
{ name: "Главная", url: "/" }, { name: "Главная", url: "/" },
{ name: "Тарифы", url: "/tariffs" }, { name: "Тарифы", url: "/tariffs" },
{ name: "Тарифы на время", url: "/tariffs/time" }, { name: "Тарифы на время", url: "/tariffs/time" },
{ name: "Тарифы на объём", url: "/tariffs/volume" }, { name: "Тарифы на объём", url: "/tariffs/volume" },
{ name: "Кастомный тариф", url: "/tariffconstructor" }, { name: "Кастомный тариф", url: "/tariffconstructor" },
{ name: "Вопросы и ответы", url: "/faq" }, { name: "Вопросы и ответы", url: "/faq" },
{ name: "История", url: "/history" }, { name: "История", url: "/history" },
{ name: "Оплата", url: "/payment" }, { name: "Оплата", url: "/payment" },
{ name: "Поддержка", url: "/support" }, { name: "Поддержка", url: "/support" },
{ name: "Оплата", url: "/wallet" }, { name: "Оплата", url: "/wallet" },
{ name: "Корзина", url: "/cart" }, { name: "Корзина", url: "/cart" },
]; ];
const Transition = React.forwardRef(function Transition( const Transition = React.forwardRef(function Transition(
props: TransitionProps & { props: TransitionProps & {
children: React.ReactElement; children: React.ReactElement;
}, },
ref: React.Ref<null> ref: React.Ref<null>
) { ) {
return <Slide direction={"left"} ref={ref} {...props} />; return <Slide direction={"left"} ref={ref} {...props} />;
}); });
interface DialogMenuProps { interface DialogMenuProps {
open: boolean; open: boolean;
handleClose: () => void; handleClose: () => void;
} }
export default function DialogMenu({ open, handleClose }: DialogMenuProps) { export default function DialogMenu({ open, handleClose }: DialogMenuProps) {
const theme = useTheme(); const theme = useTheme();
const location = useLocation(); const location = useLocation();
const isMobile = useMediaQuery(theme.breakpoints.down(600)); const isTablet = useMediaQuery(theme.breakpoints.down(900));
const user = useUserStore((state) => state.user); const user = useUserStore((state) => state.user);
const cash = useUserStore(state => state.userAccount?.wallet.cash) ?? 0; const cash = useUserStore((state) => state.userAccount?.wallet.cash) ?? 0;
return ( return (
<Dialog <Dialog
fullScreen fullScreen
sx={{ width: isMobile ? "100%" : "320px", ml: "auto", height: "100%" }} sx={{
open={open} width: isTablet ? "100%" : "320px",
onClose={handleClose} ml: "auto",
TransitionComponent={Transition} mt: "50px",
height: "100%",
".MuiBackdrop-root.MuiModal-backdrop": {
background: "transparent",
},
".MuiPaper-root.MuiPaper-rounded": {
background: "#333647",
},
}}
open={open}
onClose={handleClose}
TransitionComponent={Transition}
>
<List
sx={{
background: location.pathname === "/" ? "#333647" : "#FFFFFF",
height: "100vh",
p: "0",
paddingTop: "20px",
}}
>
<ListItem
sx={{
pl: "40px",
flexDirection: "column",
alignItems: isTablet ? "start" : "end",
}}
> >
<AppBar {arrayMenu.map(({ name, url }, index) => (
sx={{ <Button
position: "relative", key={index}
background: location.pathname === "/" ? "#333647" : "#FFFFFF", component={Link}
boxShadow: "none", to={url}
height: isMobile ? "66px" : "100px", onClick={handleClose}
}} state={user ? undefined : { backgroundLocation: location }}
disableRipple
variant="text"
sx={{
fontWeight: "500",
color:
location.pathname === url
? "#7E2AEA"
: location.pathname === "/"
? "white"
: "black",
height: "20px",
textTransform: "none",
marginBottom: "25px",
fontSize: "16px",
"&:hover": {
background: "none",
color: "#7E2AEA",
},
}}
> >
<Toolbar {name}
sx={{ </Button>
display: "flex", ))}
justifyContent: "space-between", </ListItem>
svg: { color: "#000000" }, {isTablet ? (
}} location.pathname === "/" ? (
<Button
component={Link}
to={user ? "/tariffs" : "/signin"}
state={user ? undefined : { backgroundLocation: location }}
variant="outlined"
sx={{
width: "188px",
color: "white",
border: "1px solid white",
ml: "40px",
mt: "35px",
textTransform: "none",
fontWeight: "400",
fontSize: "18px",
lineHeight: "24px",
borderRadius: "8px",
padding: "8px 17px",
}}
>
Личный кабинет
</Button>
) : (
<Box
sx={{
width: "100%",
height: "72px",
background: "#F2F3F7",
display: "flex",
alignItems: "center",
position: "absolute",
bottom: "0",
}}
>
<CustomAvatar />
<Box sx={{ ml: "8px", whiteSpace: "nowrap" }}>
<Typography
sx={{
fontSize: "12px",
lineHeight: "14px",
color: theme.palette.grey3.main,
}}
> >
{isMobile && ( Мой баланс
<Box sx={{ mt: "6px" }}> </Typography>
<img src={location.pathname === "/" ? logotip : logotipBlack} alt="icon" /> <Typography
</Box> variant="body2"
)} color={theme.palette.brightPurple.main}
<IconButton sx={{ ml: "auto" }} edge="start" color="inherit" onClick={handleClose} aria-label="close"> >
<CloseIcon /> {currencyFormatter.format(cash / 100)}
</IconButton> </Typography>
</Toolbar> </Box>
</AppBar> </Box>
<List sx={{ background: location.pathname === "/" ? "#333647" : "#FFFFFF", height: "100vh", p: "0" }}> )
<ListItem sx={{ pl: "40px", flexDirection: "column", alignItems: isMobile ? "start" : "end" }}> ) : (
{arrayMenu.map(({ name, url }, index) => ( <>
<Button <Button
key={index} component={Link}
component={Link} to={user ? "/tariffs" : "/signin"}
to={url} state={user ? undefined : { backgroundLocation: location }}
onClick={handleClose} variant="outlined"
state={user ? undefined : { backgroundLocation: location }} sx={{
disableRipple width: "188px",
variant="text" color: "white",
sx={{ border: "1px solid white",
fontWeight: "500", margin: "35px 0 0 126px",
color: location.pathname === url ? "#7E2AEA" : location.pathname === "/" ? "white" : "black", textTransform: "none",
height: "20px", fontWeight: "400",
textTransform: "none", fontSize: "18px",
marginBottom: "25px", lineHeight: "24px",
fontSize: "16px", borderRadius: "8px",
"&:hover": { padding: "8px 17px",
background: "none", }}
color: "#7E2AEA", >
}, Личный кабинет
}} </Button>
> <Box
{name} sx={{
</Button> position: "absolute",
))} right: "40px",
</ListItem> bottom: "60px",
{isMobile ? ( }}
location.pathname === "/" ? ( >
<Button <img
component={Link} src={location.pathname === "/" ? logotip : logotipBlack}
to={user ? "/tariffs" : "/signin"} alt="icon"
state={user ? undefined : { backgroundLocation: location }} />
variant="outlined" </Box>
sx={{ </>
width: "188px", )}
color: "white", </List>
border: "1px solid white", </Dialog>
ml: "40px", );
mt: "35px",
textTransform: "none",
fontWeight: "400",
fontSize: "18px",
lineHeight: "24px",
borderRadius: "8px",
padding: "8px 17px",
}}
>
Личный кабинет
</Button>
) : (
<Box
sx={{
width: "100%",
height: "72px",
background: "#F2F3F7",
display: "flex",
alignItems: "center",
position: "absolute",
bottom: "0",
}}
>
<CustomAvatar />
<Box sx={{ ml: "8px", whiteSpace: "nowrap" }}>
<Typography
sx={{
fontSize: "12px",
lineHeight: "14px",
color: theme.palette.grey3.main,
}}
>
Мой баланс
</Typography>
<Typography variant="body2" color={theme.palette.brightPurple.main}>
{currencyFormatter.format(cash / 100)}
</Typography>
</Box>
</Box>
)
) : (
<>
<Button
component={Link}
to={user ? "/tariffs" : "/signin"}
state={user ? undefined : { backgroundLocation: location }}
variant="outlined"
sx={{
width: "188px",
color: "white",
border: "1px solid white",
margin: "35px 0 0 126px",
textTransform: "none",
fontWeight: "400",
fontSize: "18px",
lineHeight: "24px",
borderRadius: "8px",
padding: "8px 17px",
}}
>
Личный кабинет
</Button>
<Box
sx={{
position: "absolute",
right: "40px",
bottom: "60px",
}}
>
<img src={location.pathname === "/" ? logotip : logotipBlack} alt="icon" />
</Box>
</>
)}
</List>
</Dialog>
);
} }

@ -1,12 +1,12 @@
import { useState } from "react"; import { useState } from "react";
import { IconButton, useTheme } from "@mui/material"; import { IconButton, useTheme } from "@mui/material";
import MenuIcon from "@mui/icons-material/Menu"; import MenuIcon from "@mui/icons-material/Menu";
import { Link } from "react-router-dom";
import SectionWrapper from "../SectionWrapper"; import SectionWrapper from "../SectionWrapper";
import PenaLogo from "../PenaLogo"; import PenaLogo from "../PenaLogo";
import DialogMenu from "./DialogMenu"; import DialogMenu from "./DialogMenu";
import { Link } from "react-router-dom";
interface Props { interface Props {
isLoggedIn: boolean; isLoggedIn: boolean;
@ -30,11 +30,10 @@ export default function NavbarCollapsed({ isLoggedIn }: Props) {
component="nav" component="nav"
maxWidth="lg" maxWidth="lg"
outerContainerSx={{ outerContainerSx={{
position: "fixed",
top: "0",
backgroundColor: theme.palette.navbarbg.main, backgroundColor: theme.palette.navbarbg.main,
position: "sticky", borderBottom: "1px solid #E3E3E3",
top: 0,
zIndex: 1,
// borderBottom: "1px solid #E3E3E3",
}} }}
sx={{ sx={{
height: "51px", height: "51px",
@ -44,9 +43,14 @@ export default function NavbarCollapsed({ isLoggedIn }: Props) {
alignItems: "center", alignItems: "center",
}} }}
> >
<Link to="/"><PenaLogo width={100} /></Link> <Link to="/">
<PenaLogo width={100} />
<IconButton onClick={handleClickOpen} sx={{ p: 0, width: "30px", color: theme.palette.primary.main }}> </Link>
<IconButton
onClick={handleClickOpen}
sx={{ p: 0, width: "30px", color: theme.palette.primary.main }}
>
<MenuIcon sx={{ height: "30px", width: "30px" }} /> <MenuIcon sx={{ height: "30px", width: "30px" }} />
</IconButton> </IconButton>
<DialogMenu open={open} handleClose={handleClose} /> <DialogMenu open={open} handleClose={handleClose} />

@ -1,5 +1,12 @@
import { Link, useLocation, useNavigate } from "react-router-dom"; import { Link, useLocation, useNavigate } from "react-router-dom";
import { Box, Button, Container, IconButton, Typography, useTheme } from "@mui/material"; import {
Box,
Button,
Container,
IconButton,
Typography,
useTheme,
} from "@mui/material";
import SectionWrapper from "../SectionWrapper"; import SectionWrapper from "../SectionWrapper";
import LogoutIcon from "../icons/LogoutIcon"; import LogoutIcon from "../icons/LogoutIcon";
import WalletIcon from "../icons/WalletIcon"; import WalletIcon from "../icons/WalletIcon";
@ -15,119 +22,129 @@ import { clearCustomTariffs } from "@root/stores/customTariffs";
import { currencyFormatter } from "@root/utils/currencyFormatter"; import { currencyFormatter } from "@root/utils/currencyFormatter";
interface Props { interface Props {
isLoggedIn: boolean; isLoggedIn: boolean;
} }
export default function NavbarFull({ isLoggedIn }: Props) { export default function NavbarFull({ isLoggedIn }: Props) {
const theme = useTheme(); const theme = useTheme();
const location = useLocation(); const location = useLocation();
const navigate = useNavigate(); const navigate = useNavigate();
const user = useUserStore((state) => state.user); const user = useUserStore((state) => state.user);
const cash = useUserStore(state => state.userAccount?.wallet.cash) ?? 0; const cash = useUserStore((state) => state.userAccount?.wallet.cash) ?? 0;
async function handleLogoutClick() { async function handleLogoutClick() {
try { try {
await logout(); await logout();
clearAuthToken(); clearAuthToken();
clearUserData(); clearUserData();
clearCustomTariffs(); clearCustomTariffs();
navigate("/"); navigate("/");
} catch (error: any) { } catch (error: any) {
const message = getMessageFromFetchError(error, "Не удалось выйти"); const message = getMessageFromFetchError(error, "Не удалось выйти");
if (message) enqueueSnackbar(message); if (message) enqueueSnackbar(message);
}
} }
}
return isLoggedIn ? ( return isLoggedIn ? (
<Container <Container
component="nav" component="nav"
disableGutters disableGutters
maxWidth={false} maxWidth={false}
sx={{ sx={{
px: "16px", px: "16px",
display: "flex", display: "flex",
height: "80px", height: "80px",
alignItems: "center", alignItems: "center",
gap: "60px", gap: "60px",
bgcolor: "white", bgcolor: "white",
borderBottom: "1px solid #E3E3E3", borderBottom: "1px solid #E3E3E3",
}} }}
>
<Link to="/">
<PenaLogo width={124} />
</Link>
<Menu />
<Box
sx={{
display: "flex",
ml: "auto",
}}
>
<Drawers />
<IconButton
sx={{ p: 0, ml: "8px" }}
onClick={() => navigate("/wallet")}
> >
<Link to="/"><PenaLogo width={124} /></Link> <WalletIcon color={theme.palette.grey2.main} bgcolor="#F2F3F7" />
<Menu /> </IconButton>
<Box <Box sx={{ ml: "8px", whiteSpace: "nowrap" }}>
sx={{ <Typography
display: "flex", sx={{
ml: "auto", fontSize: "12px",
}} lineHeight: "14px",
> color: theme.palette.grey3.main,
<Drawers /> }}
<IconButton >
sx={{ p: 0, ml: "8px" }} Мой баланс
onClick={() => navigate("/wallet")} </Typography>
> <Typography variant="body2" color={theme.palette.brightPurple.main}>
<WalletIcon color={theme.palette.grey2.main} bgcolor="#F2F3F7" /> {currencyFormatter.format(cash / 100)}
</IconButton> </Typography>
<Box sx={{ ml: "8px", whiteSpace: "nowrap" }}> </Box>
<Typography <CustomAvatar />
sx={{ <IconButton
fontSize: "12px", onClick={handleLogoutClick}
lineHeight: "14px", sx={{
color: theme.palette.grey3.main, ml: "20px",
}} bgcolor: "#F2F3F7",
> borderRadius: "6px",
Мой баланс height: "36px",
</Typography> width: "36px",
<Typography variant="body2" color={theme.palette.brightPurple.main}> }}
{currencyFormatter.format(cash / 100)} >
</Typography> <LogoutIcon />
</Box> </IconButton>
<CustomAvatar /> </Box>
<IconButton </Container>
onClick={handleLogoutClick} ) : (
sx={{ ml: "20px", bgcolor: "#F2F3F7", borderRadius: "6px", height: "36px", width: "36px" }} <>
> <SectionWrapper
<LogoutIcon /> component="nav"
</IconButton> maxWidth="lg"
</Box> outerContainerSx={{
</Container> position: "fixed",
) : ( top: "0",
<> backgroundColor: theme.palette.lightPurple.main,
<SectionWrapper borderBottom: "1px solid #E3E3E3",
component="nav" }}
maxWidth="lg" sx={{
outerContainerSx={{ px: "20px",
backgroundColor: theme.palette.lightPurple.main, display: "flex",
borderBottom: "1px solid #E3E3E3", justifyContent: "space-between",
}} height: "80px",
sx={{ alignItems: "center",
px: "20px", gap: "50px",
display: "flex", }}
justifyContent: "space-between", >
height: "80px", <PenaLogo width={150} />
alignItems: "center", <Menu />
gap: "50px", <Button
}} component={Link}
> to={user ? "/tariffs" : "/signin"}
<PenaLogo width={150} /> state={user ? undefined : { backgroundLocation: location }}
<Menu /> variant="outlined"
<Button sx={{
component={Link} px: "18px",
to={user ? "/tariffs" : "/signin"} py: "10px",
state={user ? undefined : { backgroundLocation: location }} borderColor: "white",
variant="outlined" borderRadius: "8px",
sx={{ whiteSpace: "nowrap",
px: "18px", minWidth: "180px",
py: "10px", }}
borderColor: "white", >
borderRadius: "8px", Личный кабинет
whiteSpace: "nowrap", </Button>
minWidth: "180px", </SectionWrapper>
}} </>
> );
Личный кабинет
</Button>
</SectionWrapper>
</>
);
} }

@ -6,10 +6,12 @@ import {
useTheme, useTheme,
useMediaQuery, useMediaQuery,
} from "@mui/material"; } from "@mui/material";
import { Link } from "react-router-dom";
type Notification = { type Notification = {
text: string; text: string;
date: string; date: string;
url: string;
watched?: boolean; watched?: boolean;
}; };
@ -59,58 +61,68 @@ export const NotificationsModal = ({
}} }}
> >
<List sx={{ width: "100%", padding: "5px" }}> <List sx={{ width: "100%", padding: "5px" }}>
{notifications.map(({ text, date, watched = true }) => ( {notifications.map(({ text, date, url, watched = true }) => (
<ListItem <Link
sx={{ to={url}
display: "flex", onClick={() => setOpen(false)}
alignItems: isMobile ? "normal" : "center", style={{
justifyContent: "space-between", textDecoration: "none",
flexDirection: isMobile ? "column-reverse" : "unset", color: "inherit",
borderBottom: "1px solid #F2F3F7",
padding: "20px 10px",
background: watched ? "none" : "#F0F0F6",
borderRadius: watched ? "0" : "8px",
"&:first-child": {
borderTop: "1px solid #F2F3F7",
},
}} }}
> >
<Typography <ListItem
key={text + date}
sx={{ sx={{
position: "relative", display: "flex",
fontSize: "16px", alignItems: isMobile ? "normal" : "center",
lineHeight: "19px", justifyContent: "space-between",
paddingLeft: watched ? "0" : "35px", flexDirection: isMobile ? "column-reverse" : "unset",
fontWeight: watched ? "normal" : "bold", borderBottom: "1px solid #F2F3F7",
"&::before": { padding: "20px 10px",
content: '""', background: watched ? "none" : "#F0F0F6",
display: watched ? "none" : "block", borderRadius: watched ? "0" : "8px",
position: "absolute", "&:first-child": {
left: "10px", borderTop: "1px solid #F2F3F7",
top: isMobile ? "5px" : "50%",
transform: isMobile ? "none" : "translateY(-50%)",
height: "8px",
width: "8px",
borderRadius: "50%",
background: "#7E2AEA",
}, },
}} }}
> >
{text} <Typography
</Typography> sx={{
<Typography position: "relative",
sx={{ fontSize: "16px",
fontSize: "14px", lineHeight: "19px",
lineHeight: "19px", paddingLeft: watched ? "0" : "35px",
color: "#9A9AAF", fontWeight: watched ? "normal" : "bold",
fontWeight: watched ? "normal" : "bold", "&::before": {
paddingLeft: isMobile ? (watched ? "0" : "35px") : "0", content: '""',
marginBottom: isMobile ? "5px" : "0", display: watched ? "none" : "block",
}} position: "absolute",
> left: "10px",
{date} top: isMobile ? "5px" : "50%",
</Typography> transform: isMobile ? "none" : "translateY(-50%)",
</ListItem> height: "8px",
width: "8px",
borderRadius: "50%",
background: "#7E2AEA",
},
}}
>
{text}
</Typography>
<Typography
sx={{
fontSize: "14px",
lineHeight: "19px",
color: "#9A9AAF",
fontWeight: watched ? "normal" : "bold",
paddingLeft: isMobile ? (watched ? "0" : "35px") : "0",
marginBottom: isMobile ? "5px" : "0",
}}
>
{date}
</Typography>
</ListItem>
</Link>
))} ))}
</List> </List>
</Popover> </Popover>

@ -39,7 +39,7 @@ export const Select = ({
<Box> <Box>
<Box <Box
sx={{ sx={{
zIndex: 1500, zIndex: 1,
position: "relative", position: "relative",
width: "100%", width: "100%",
height: "56px", height: "56px",
@ -81,6 +81,7 @@ export const Select = ({
<MuiSelect <MuiSelect
ref={ref} ref={ref}
className="select" className="select"
value=""
open={opened} open={opened}
MenuProps={{ disablePortal: true }} MenuProps={{ disablePortal: true }}
sx={{ width: "100%" }} sx={{ width: "100%" }}

@ -26,3 +26,7 @@
.MuiInputBase-root.MuiOutlinedInput-root .MuiSelect-icon { .MuiInputBase-root.MuiOutlinedInput-root .MuiSelect-icon {
display: none; display: none;
} }
.MuiMenu-root.MuiModal-root {
z-index: 0;
}

@ -22,9 +22,7 @@ interface Props {
color?: string; color?: string;
FormInputSx?: SxProps<Theme>; FormInputSx?: SxProps<Theme>;
TextfieldProps: TextFieldProps; TextfieldProps: TextFieldProps;
onChange: ( onChange: (e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => void;
e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>
) => void;
} }
export default function PasswordInput({ export default function PasswordInput({
@ -46,17 +44,13 @@ export default function PasswordInput({
: { ...theme.typography.body1, fontWeight: 500 } : { ...theme.typography.body1, fontWeight: 500 }
: theme.typography.body2; : theme.typography.body2;
const placeholderFont = upMd const placeholderFont = upMd ? undefined : { fontWeight: 400, fontSize: "16px", lineHeight: "19px" };
? undefined
: { fontWeight: 400, fontSize: "16px", lineHeight: "19px" };
const [showPassword, setShowPassword] = React.useState(false); const [showPassword, setShowPassword] = React.useState(false);
const handleClickShowPassword = () => setShowPassword((show) => !show); const handleClickShowPassword = () => setShowPassword((show) => !show);
const handleMouseDownPassword = ( const handleMouseDownPassword = (event: React.MouseEvent<HTMLButtonElement>) => {
event: React.MouseEvent<HTMLButtonElement>
) => {
event.preventDefault(); event.preventDefault();
}; };

@ -20,6 +20,7 @@ export default function Landing({ templaterOnly = false }: Props) {
<Box <Box
sx={{ sx={{
position: "relative", position: "relative",
paddingTop: "80px",
}} }}
> >
<Navbar isLoggedIn={false} /> <Navbar isLoggedIn={false} />

@ -46,6 +46,10 @@
right: -5px; right: -5px;
} }
.slider .slick-arrow::before {
display: none;
}
.slider .slick-dots { .slider .slick-dots {
bottom: -15px; bottom: -15px;
} }

@ -12,19 +12,23 @@ import "slick-carousel/slick/slick-theme.css";
import "./slider.css"; import "./slider.css";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import type { CustomArrowProps } from "react-slick";
type SliderProps = { type SliderProps = {
items: ReactNode[]; items: ReactNode[];
}; };
type ArrowProps = CustomArrowProps & {
icon: ReactNode;
};
export const Slider = ({ items }: SliderProps) => { export const Slider = ({ items }: SliderProps) => {
const [range, setRange] = useState<number>(3); const [range, setRange] = useState<number>(3);
const [activeRange, setActiveRange] = useState<number[]>([0, 1, 2]); const [activeRange, setActiveRange] = useState<number[]>([0, 1, 2]);
const theme = useTheme(); const theme = useTheme();
const isMiddle = useMediaQuery( const isMiddle = useMediaQuery(theme.breakpoints.down(1200));
theme.breakpoints.down(1200) && theme.breakpoints.up(830)
);
const isTablet = useMediaQuery(theme.breakpoints.down(830)); const isTablet = useMediaQuery(theme.breakpoints.down(830));
const isMobileHeader = useMediaQuery(theme.breakpoints.down(900));
useEffect(() => { useEffect(() => {
if (isTablet) { if (isTablet) {
@ -64,10 +68,14 @@ export const Slider = ({ items }: SliderProps) => {
]; ];
}; };
const Arrow = ({ currentSlide, slideCount, icon, ...props }: ArrowProps) => (
<Box {...props}>{icon}</Box>
);
return ( return (
<Box className="slider-wrapper"> <Box className="slider-wrapper">
{(items.length < 4 && !isMiddle && !isTablet) || {(items.length < 4 && !isMiddle && !isTablet) ||
(items.length < 3 && isMiddle) || (items.length < 3 && isMiddle && !isTablet) ||
(items.length < 1 && isTablet) ? ( (items.length < 1 && isTablet) ? (
<Box <Box
sx={{ sx={{
@ -87,14 +95,15 @@ export const Slider = ({ items }: SliderProps) => {
dots dots
infinite infinite
variableWidth variableWidth
lazyLoad={isMobileHeader ? "progressive" : undefined}
slidesToShow={range} slidesToShow={range}
prevArrow={<img src={arrowLeftIcon} alt="prev" />} prevArrow={<Arrow icon={<img src={arrowLeftIcon} alt="prev" />} />}
nextArrow={<img src={arrowRightIcon} alt="next" />} nextArrow={<Arrow icon={<img src={arrowRightIcon} alt="next" />} />}
beforeChange={(_, active) => beforeChange={(_, active) =>
setActiveRange(calculateRange(active, items.length)) setActiveRange(calculateRange(active, items.length))
} }
customPaging={(slideNumber) => ( customPaging={(slideNumber) => (
<li <Box
className={classNames("dot", { className={classNames("dot", {
active: activeRange.includes(slideNumber), active: activeRange.includes(slideNumber),
})} })}

@ -76,39 +76,39 @@ export default function SigninDialog() {
setTimeout(() => navigate("/"), theme.transitions.duration.leavingScreen); setTimeout(() => navigate("/"), theme.transitions.duration.leavingScreen);
} }
return ( return (
<Dialog <Dialog
open={isDialogOpen} open={isDialogOpen}
onClose={handleClose} onClose={handleClose}
PaperProps={{ PaperProps={{
sx: { sx: {
width: "600px", width: "600px",
maxWidth: "600px", maxWidth: "600px",
} },
}} }}
slotProps={{ slotProps={{
backdrop: { backdrop: {
style: { style: {
backgroundColor: "rgb(0 0 0 / 0.7)", backgroundColor: "rgb(0 0 0 / 0.7)",
} },
} },
}} }}
> >
<Box <Box
component="form" component="form"
onSubmit={formik.handleSubmit} onSubmit={formik.handleSubmit}
noValidate noValidate
sx={{ sx={{
position: "relative", position: "relative",
backgroundColor: "white", backgroundColor: "white",
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
flexDirection: "column", flexDirection: "column",
p: upMd ? "50px" : "18px", p: upMd ? "50px" : "18px",
pb: upMd ? "40px" : "30px", pb: upMd ? "40px" : "30px",
gap: "15px", gap: "15px",
borderRadius: "12px", borderRadius: "12px",
boxShadow: cardShadow, boxShadow: cardShadow,
}} }}
> >
<IconButton <IconButton

@ -16,193 +16,203 @@ import { makeRequest } from "@frontend/kitui";
import { cardShadow } from "@root/utils/themes/shadow"; import { cardShadow } from "@root/utils/themes/shadow";
import PasswordInput from "@root/components/passwordInput"; import PasswordInput from "@root/components/passwordInput";
interface Values { interface Values {
login: string; login: string;
password: string; password: string;
repeatPassword: string; repeatPassword: string;
} }
const initialValues: Values = { const initialValues: Values = {
login: "", login: "",
password: "", password: "",
repeatPassword: "", repeatPassword: "",
}; };
const validationSchema = object({ const validationSchema = object({
login: string().required("Поле обязательно"), login: string().required("Поле обязательно"),
password: string().min(8, "Минимум 8 символов").matches(/^[.,:;-_+\d\w]+$/, "Некорректные символы").required("Поле обязательно"), password: string()
repeatPassword: string().oneOf([ref("password"), undefined], "Пароли не совпадают"), .min(8, "Минимум 8 символов")
.matches(/^[.,:;-_+\d\w]+$/, "Некорректные символы")
.required("Поле обязательно"),
repeatPassword: string()
.oneOf([ref("password"), undefined], "Пароли не совпадают")
.required("Повторите пароль"),
}); });
export default function SignupDialog() { export default function SignupDialog() {
const [isDialogOpen, setIsDialogOpen] = useState<boolean>(true); const [isDialogOpen, setIsDialogOpen] = useState<boolean>(true);
const user = useUserStore(state => state.user); const user = useUserStore((state) => state.user);
const theme = useTheme(); const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md")); const upMd = useMediaQuery(theme.breakpoints.up("md"));
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const formik = useFormik<Values>({ const formik = useFormik<Values>({
initialValues, initialValues,
validationSchema, validationSchema,
onSubmit: (values, formikHelpers) => { onSubmit: (values, formikHelpers) => {
makeRequest<RegisterRequest, RegisterResponse>({ makeRequest<RegisterRequest, RegisterResponse>({
url: "https://hub.pena.digital/auth/register", url: "https://hub.pena.digital/auth/register",
body: { body: {
login: values.login.trim(), login: values.login.trim(),
password: values.password.trim(), password: values.password.trim(),
phoneNumber: "+7", phoneNumber: "+7",
},
useToken: false,
withCredentials: true,
}).then(result => {
setUserId(result._id);
}).catch((error: any) => {
const errorMessage = getMessageFromFetchError(error);
if (errorMessage) enqueueSnackbar(errorMessage);
}).finally(() => {
formikHelpers.setSubmitting(false);
});
}, },
}); useToken: false,
withCredentials: true,
})
.then((result) => {
setUserId(result._id);
})
.catch((error: any) => {
const errorMessage = getMessageFromFetchError(error);
if (errorMessage) enqueueSnackbar(errorMessage);
})
.finally(() => {
formikHelpers.setSubmitting(false);
});
},
});
useEffect(function redirectIfSignedIn() { useEffect(
if (user) navigate("/tariffs", { replace: true }); function redirectIfSignedIn() {
}, [navigate, user]); if (user) navigate("/tariffs", { replace: true });
},
[navigate, user]
);
function handleClose() { function handleClose() {
setIsDialogOpen(false); setIsDialogOpen(false);
setTimeout(() => navigate("/"), theme.transitions.duration.leavingScreen); setTimeout(() => navigate("/"), theme.transitions.duration.leavingScreen);
} }
return ( return (
<Dialog <Dialog
open={isDialogOpen} open={isDialogOpen}
onClose={handleClose} onClose={handleClose}
PaperProps={{ PaperProps={{
sx: { sx: {
width: "600px", width: "600px",
maxWidth: "600px", maxWidth: "600px",
} },
}} }}
slotProps={{ slotProps={{
backdrop: { backdrop: {
style: { style: {
backgroundColor: "rgb(0 0 0 / 0.7)", backgroundColor: "rgb(0 0 0 / 0.7)",
} },
} },
}} }}
>
<Box
component="form"
onSubmit={formik.handleSubmit}
noValidate
sx={{
position: "relative",
backgroundColor: "white",
display: "flex",
alignItems: "center",
flexDirection: "column",
p: upMd ? "50px" : "18px",
pb: upMd ? "40px" : "30px",
gap: "15px",
borderRadius: "12px",
boxShadow: cardShadow,
}}
>
<IconButton
onClick={handleClose}
sx={{
position: "absolute",
right: "7px",
top: "7px",
}}
> >
<Box <CloseIcon sx={{ transform: "scale(1.5)" }} />
component="form" </IconButton>
onSubmit={formik.handleSubmit} <Box sx={{ mt: upMd ? undefined : "62px" }}>
noValidate <PenaLogo width={upMd ? 233 : 196} />
sx={{ </Box>
position: "relative", <Typography
backgroundColor: "white", sx={{
display: "flex", color: theme.palette.grey3.main,
alignItems: "center", mt: "5px",
flexDirection: "column", mb: upMd ? "35px" : "33px",
p: upMd ? "50px" : "18px", }}
pb: upMd ? "40px" : "30px", >
gap: "15px", Регистрация
borderRadius: "12px", </Typography>
boxShadow: cardShadow, <InputTextfield
}} TextfieldProps={{
> value: formik.values.login,
<IconButton placeholder: "username",
onClick={handleClose} onBlur: formik.handleBlur,
sx={{ error: formik.touched.login && Boolean(formik.errors.login),
position: "absolute", helperText: formik.touched.login && formik.errors.login,
right: "7px", }}
top: "7px", onChange={formik.handleChange}
}} color="#F2F3F7"
> id="login"
<CloseIcon sx={{ transform: "scale(1.5)" }} /> label="Логин"
</IconButton> gap={upMd ? "10px" : "10px"}
<Box sx={{ mt: upMd ? undefined : "62px" }}> />
<PenaLogo width={upMd ? 233 : 196} /> <PasswordInput
</Box> TextfieldProps={{
<Typography value: formik.values.password,
sx={{ placeholder: "Не менее 8 символов",
color: theme.palette.grey3.main, onBlur: formik.handleBlur,
mt: "5px", error: formik.touched.password && Boolean(formik.errors.password),
mb: upMd ? "35px" : "33px", helperText: formik.touched.password && formik.errors.password,
}} autoComplete: "new-password",
> }}
Регистрация onChange={formik.handleChange}
</Typography> color="#F2F3F7"
<InputTextfield id="password"
TextfieldProps={{ label="Пароль"
value: formik.values.login, gap={upMd ? "10px" : "10px"}
placeholder: "username", />
onBlur: formik.handleBlur, <PasswordInput
error: formik.touched.login && Boolean(formik.errors.login), TextfieldProps={{
helperText: formik.touched.login && formik.errors.login, value: formik.values.repeatPassword,
}} placeholder: "Не менее 8 символов",
onChange={formik.handleChange} onBlur: formik.handleBlur,
color="#F2F3F7" error: formik.touched.repeatPassword && Boolean(formik.errors.repeatPassword),
id="login" helperText: formik.touched.repeatPassword && formik.errors.repeatPassword,
label="Логин" autoComplete: "new-password",
gap={upMd ? "10px" : "10px"} }}
/> onChange={formik.handleChange}
<PasswordInput color="#F2F3F7"
TextfieldProps={{ id="repeatPassword"
value: formik.values.password, label="Повторить пароль"
placeholder: "Не менее 8 символов", gap={upMd ? "10px" : "10px"}
onBlur: formik.handleBlur, />
error: formik.touched.password && Boolean(formik.errors.password), <CustomButton
helperText: formik.touched.password && formik.errors.password, fullWidth
autoComplete: "new-password" variant="contained"
}} sx={{
onChange={formik.handleChange} backgroundColor: theme.palette.brightPurple.main,
color="#F2F3F7" textColor: "white",
id="password" width: "100%",
label="Пароль" py: "12px",
gap={upMd ? "10px" : "10px"} mt: upMd ? undefined : "10px",
/> }}
<PasswordInput type="submit"
TextfieldProps={{ disabled={formik.isSubmitting}
value: formik.values.repeatPassword, >
placeholder: "Не менее 8 символов", Зарегистрироваться
onBlur: formik.handleBlur, </CustomButton>
error: formik.touched.repeatPassword && Boolean(formik.errors.repeatPassword), <Link
helperText: formik.touched.repeatPassword && formik.errors.repeatPassword, component={RouterLink}
autoComplete: "new-password" to="/signin"
}} state={{ backgroundLocation: location.state.backgroundLocation }}
onChange={formik.handleChange} sx={{
color="#F2F3F7" color: theme.palette.brightPurple.main,
id="repeatPassword" mt: "auto",
label="Повторить пароль" }}
gap={upMd ? "10px" : "10px"} >
/> Вход в личный кабинет
<CustomButton </Link>
fullWidth </Box>
variant="contained" </Dialog>
sx={{ );
backgroundColor: theme.palette.brightPurple.main,
textColor: "white",
width: "100%",
py: "12px",
mt: upMd ? undefined : "10px",
}}
type="submit"
disabled={formik.isSubmitting}
>
Зарегистрироваться
</CustomButton>
<Link
component={RouterLink}
to="/signin"
state={{ backgroundLocation: location.state.backgroundLocation }}
sx={{
color: theme.palette.brightPurple.main,
mt: "auto",
}}
>
Вход в личный кабинет
</Link>
</Box>
</Dialog>
);
} }

@ -655,7 +655,7 @@ const templategenTariff1: Tariff = {
price: 0, price: 0,
isCustom: false, isCustom: false,
privilegies: [ privilegies: [
{ {
_id: "p1", _id: "p1",
name: "n1", name: "n1",
privilegeId: "p1", privilegeId: "p1",
@ -678,7 +678,7 @@ const templategenTariff2: Tariff = {
price: 0, price: 0,
isCustom: false, isCustom: false,
privilegies: [ privilegies: [
{ {
_id: "p5", _id: "p5",
name: "n5", name: "n5",
privilegeId: "p5", privilegeId: "p5",

@ -1,8 +1 @@
export const cardShadow = ` export const cardShadow = "0px 15px 80px rgb(210 208 225 / 70%)";
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)
`;

@ -1,27 +1,22 @@
{ {
"extends": "./tsconfig.extend.json", "extends": "./tsconfig.extend.json",
"compilerOptions": { "compilerOptions": {
"target": "es5", "target": "es5",
"lib": [ "lib": ["es5", "dom", "dom.iterable", "esnext"],
"dom", "types": ["node"],
"dom.iterable", "allowJs": true,
"esnext" "skipLibCheck": true,
], "esModuleInterop": true,
"allowJs": true, "allowSyntheticDefaultImports": true,
"skipLibCheck": true, "strict": true,
"esModuleInterop": true, "forceConsistentCasingInFileNames": true,
"allowSyntheticDefaultImports": true, "noFallthroughCasesInSwitch": true,
"strict": true, "module": "esnext",
"forceConsistentCasingInFileNames": true, "moduleResolution": "node",
"noFallthroughCasesInSwitch": true, "resolveJsonModule": true,
"module": "esnext", "isolatedModules": true,
"moduleResolution": "node", "noEmit": true,
"resolveJsonModule": true, "jsx": "react-jsx"
"isolatedModules": true, },
"noEmit": true, "include": ["src", "**/*.ts"]
"jsx": "react-jsx" }
},
"include": [
"src"
]
}

710
yarn.lock

File diff suppressed because it is too large Load Diff