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");
});
});

@ -7,7 +7,9 @@
"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",
"cypress": "cypress open"
}, },
"dependencies": { "dependencies": {
"@emotion/react": "^11.10.5", "@emotion/react": "^11.10.5",
@ -19,6 +21,7 @@
"axios": "^1.4.0", "axios": "^1.4.0",
"buffer": "^6.0.3", "buffer": "^6.0.3",
"classnames": "^2.3.2", "classnames": "^2.3.2",
"cypress": "^12.17.3",
"formik": "^2.2.9", "formik": "^2.2.9",
"immer": "^10.0.2", "immer": "^10.0.2",
"isomorphic-fetch": "^3.0.0", "isomorphic-fetch": "^3.0.0",

@ -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,18 +73,23 @@ 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 sx={{ height: "100%" }}>
<Box <Box
sx={{ sx={{
zIndex: 2,
position: "fixed",
top: 0,
left: 0,
width: "100%",
display: "flex", display: "flex",
columnGap: "10px", columnGap: "10px",
alignItems: "center", alignItems: "center",
height: "100%", height: "51px",
padding: "0 18px", padding: "0 18px",
background: "#FFFFFF",
}} }}
> >
<IconButton <IconButton
@ -158,10 +165,10 @@ export default function NavbarCollapsed({ isLoggedIn, children }: Props) {
}} }}
> >
<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.7) translate(50%, -50%)", transform: "scale(0.7) translate(50%, -50%)",
@ -175,12 +182,13 @@ export default function NavbarCollapsed({ isLoggedIn, children }: Props) {
</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,
}))} }))}
/> />
@ -188,6 +196,7 @@ export default function NavbarCollapsed({ isLoggedIn, children }: Props) {
<PenaLogo width={100} /> <PenaLogo width={100} />
</Link> </Link>
</Box> </Box>
</Box>
<Box sx={{ display: "flex", overflow: open ? "hidden" : "unset" }}> <Box sx={{ display: "flex", overflow: open ? "hidden" : "unset" }}>
<Drawer <Drawer
sx={{ sx={{
@ -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,18 +3,14 @@ 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,
IconButton,
List, List,
ListItem, ListItem,
Slide, Slide,
Toolbar,
Typography, Typography,
useMediaQuery, useMediaQuery,
useTheme, useTheme,
@ -55,45 +51,44 @@ interface DialogMenuProps {
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={{
width: isTablet ? "100%" : "320px",
ml: "auto",
mt: "50px",
height: "100%",
".MuiBackdrop-root.MuiModal-backdrop": {
background: "transparent",
},
".MuiPaper-root.MuiPaper-rounded": {
background: "#333647",
},
}}
open={open} open={open}
onClose={handleClose} onClose={handleClose}
TransitionComponent={Transition} TransitionComponent={Transition}
> >
<AppBar <List
sx={{ sx={{
position: "relative",
background: location.pathname === "/" ? "#333647" : "#FFFFFF", background: location.pathname === "/" ? "#333647" : "#FFFFFF",
boxShadow: "none", height: "100vh",
height: isMobile ? "66px" : "100px", p: "0",
paddingTop: "20px",
}} }}
> >
<Toolbar <ListItem
sx={{ sx={{
display: "flex", pl: "40px",
justifyContent: "space-between", flexDirection: "column",
svg: { color: "#000000" }, alignItems: isTablet ? "start" : "end",
}} }}
> >
{isMobile && (
<Box sx={{ mt: "6px" }}>
<img src={location.pathname === "/" ? logotip : logotipBlack} alt="icon" />
</Box>
)}
<IconButton sx={{ ml: "auto" }} edge="start" color="inherit" onClick={handleClose} aria-label="close">
<CloseIcon />
</IconButton>
</Toolbar>
</AppBar>
<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) => ( {arrayMenu.map(({ name, url }, index) => (
<Button <Button
key={index} key={index}
@ -105,7 +100,12 @@ export default function DialogMenu({ open, handleClose }: DialogMenuProps) {
variant="text" variant="text"
sx={{ sx={{
fontWeight: "500", fontWeight: "500",
color: location.pathname === url ? "#7E2AEA" : location.pathname === "/" ? "white" : "black", color:
location.pathname === url
? "#7E2AEA"
: location.pathname === "/"
? "white"
: "black",
height: "20px", height: "20px",
textTransform: "none", textTransform: "none",
marginBottom: "25px", marginBottom: "25px",
@ -120,7 +120,7 @@ export default function DialogMenu({ open, handleClose }: DialogMenuProps) {
</Button> </Button>
))} ))}
</ListItem> </ListItem>
{isMobile ? ( {isTablet ? (
location.pathname === "/" ? ( location.pathname === "/" ? (
<Button <Button
component={Link} component={Link}
@ -166,7 +166,10 @@ export default function DialogMenu({ open, handleClose }: DialogMenuProps) {
> >
Мой баланс Мой баланс
</Typography> </Typography>
<Typography variant="body2" color={theme.palette.brightPurple.main}> <Typography
variant="body2"
color={theme.palette.brightPurple.main}
>
{currencyFormatter.format(cash / 100)} {currencyFormatter.format(cash / 100)}
</Typography> </Typography>
</Box> </Box>
@ -201,7 +204,10 @@ export default function DialogMenu({ open, handleClose }: DialogMenuProps) {
bottom: "60px", bottom: "60px",
}} }}
> >
<img src={location.pathname === "/" ? logotip : logotipBlack} alt="icon" /> <img
src={location.pathname === "/" ? logotip : logotipBlack}
alt="icon"
/>
</Box> </Box>
</> </>
)} )}

@ -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} />
</Link>
<IconButton onClick={handleClickOpen} sx={{ p: 0, width: "30px", color: theme.palette.primary.main }}> <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";
@ -23,7 +30,7 @@ export default function NavbarFull({ isLoggedIn }: Props) {
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 {
@ -53,7 +60,9 @@ export default function NavbarFull({ isLoggedIn }: Props) {
borderBottom: "1px solid #E3E3E3", borderBottom: "1px solid #E3E3E3",
}} }}
> >
<Link to="/"><PenaLogo width={124} /></Link> <Link to="/">
<PenaLogo width={124} />
</Link>
<Menu /> <Menu />
<Box <Box
sx={{ sx={{
@ -85,7 +94,13 @@ export default function NavbarFull({ isLoggedIn }: Props) {
<CustomAvatar /> <CustomAvatar />
<IconButton <IconButton
onClick={handleLogoutClick} onClick={handleLogoutClick}
sx={{ ml: "20px", bgcolor: "#F2F3F7", borderRadius: "6px", height: "36px", width: "36px" }} sx={{
ml: "20px",
bgcolor: "#F2F3F7",
borderRadius: "6px",
height: "36px",
width: "36px",
}}
> >
<LogoutIcon /> <LogoutIcon />
</IconButton> </IconButton>
@ -97,6 +112,8 @@ export default function NavbarFull({ isLoggedIn }: Props) {
component="nav" component="nav"
maxWidth="lg" maxWidth="lg"
outerContainerSx={{ outerContainerSx={{
position: "fixed",
top: "0",
backgroundColor: theme.palette.lightPurple.main, backgroundColor: theme.palette.lightPurple.main,
borderBottom: "1px solid #E3E3E3", borderBottom: "1px solid #E3E3E3",
}} }}

@ -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,8 +61,17 @@ 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 }) => (
<Link
to={url}
onClick={() => setOpen(false)}
style={{
textDecoration: "none",
color: "inherit",
}}
>
<ListItem <ListItem
key={text + date}
sx={{ sx={{
display: "flex", display: "flex",
alignItems: isMobile ? "normal" : "center", alignItems: isMobile ? "normal" : "center",
@ -111,6 +122,7 @@ export const NotificationsModal = ({
{date} {date}
</Typography> </Typography>
</ListItem> </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),
})} })}

@ -84,14 +84,14 @@ export default function SigninDialog() {
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

@ -16,7 +16,6 @@ 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;
@ -31,13 +30,18 @@ const initialValues: Values = {
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();
@ -55,20 +59,26 @@ export default function SignupDialog() {
}, },
useToken: false, useToken: false,
withCredentials: true, withCredentials: true,
}).then(result => { })
.then((result) => {
setUserId(result._id); setUserId(result._id);
}).catch((error: any) => { })
.catch((error: any) => {
const errorMessage = getMessageFromFetchError(error); const errorMessage = getMessageFromFetchError(error);
if (errorMessage) enqueueSnackbar(errorMessage); if (errorMessage) enqueueSnackbar(errorMessage);
}).finally(() => { })
.finally(() => {
formikHelpers.setSubmitting(false); formikHelpers.setSubmitting(false);
}); });
}, },
}); });
useEffect(function redirectIfSignedIn() { useEffect(
function redirectIfSignedIn() {
if (user) navigate("/tariffs", { replace: true }); if (user) navigate("/tariffs", { replace: true });
}, [navigate, user]); },
[navigate, user]
);
function handleClose() { function handleClose() {
setIsDialogOpen(false); setIsDialogOpen(false);
@ -83,14 +93,14 @@ export default function SignupDialog() {
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
@ -153,7 +163,7 @@ export default function SignupDialog() {
onBlur: formik.handleBlur, onBlur: formik.handleBlur,
error: formik.touched.password && Boolean(formik.errors.password), error: formik.touched.password && Boolean(formik.errors.password),
helperText: formik.touched.password && formik.errors.password, helperText: formik.touched.password && formik.errors.password,
autoComplete: "new-password" autoComplete: "new-password",
}} }}
onChange={formik.handleChange} onChange={formik.handleChange}
color="#F2F3F7" color="#F2F3F7"
@ -168,7 +178,7 @@ export default function SignupDialog() {
onBlur: formik.handleBlur, onBlur: formik.handleBlur,
error: formik.touched.repeatPassword && Boolean(formik.errors.repeatPassword), error: formik.touched.repeatPassword && Boolean(formik.errors.repeatPassword),
helperText: formik.touched.repeatPassword && formik.errors.repeatPassword, helperText: formik.touched.repeatPassword && formik.errors.repeatPassword,
autoComplete: "new-password" autoComplete: "new-password",
}} }}
onChange={formik.handleChange} onChange={formik.handleChange}
color="#F2F3F7" color="#F2F3F7"

@ -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)
`;

@ -2,11 +2,8 @@
"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",
"esnext"
],
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
"esModuleInterop": true, "esModuleInterop": true,
@ -21,7 +18,5 @@
"noEmit": true, "noEmit": true,
"jsx": "react-jsx" "jsx": "react-jsx"
}, },
"include": [ "include": ["src", "**/*.ts"]
"src"
]
} }

710
yarn.lock

File diff suppressed because it is too large Load Diff