Merge branch 'WIP' into dev
This commit is contained in:
commit
5ccad90f79
@ -1,27 +1,15 @@
|
||||
import { User } from "@root/model/auth";
|
||||
import { authStore } from "@root/stores/makeRequest";
|
||||
|
||||
|
||||
const apiUrl = process.env.NODE_ENV === "production" ? "/user" : "https://hub.pena.digital/user";
|
||||
const authUrl = process.env.NODE_ENV === "production" ? "/auth" : "https://hub.pena.digital/auth";
|
||||
const apiUrl = process.env.NODE_ENV === "production" ? "" : "https://hub.pena.digital";
|
||||
|
||||
const makeRequest = authStore.getState().makeRequest;
|
||||
|
||||
export async function getUser(userId: string): Promise<User | null> {
|
||||
return makeRequest<never, User>({
|
||||
url: `${apiUrl}/${userId}`,
|
||||
contentType: true,
|
||||
method: "GET",
|
||||
useToken: false,
|
||||
withCredentials: false,
|
||||
});
|
||||
}
|
||||
|
||||
export function logout() {
|
||||
return makeRequest<never, void>({
|
||||
url: authUrl + "/logout",
|
||||
url: apiUrl + "/auth/logout",
|
||||
method: "POST",
|
||||
useToken: false,
|
||||
useToken: true,
|
||||
withCredentials: true,
|
||||
});
|
||||
}
|
28
src/api/user.ts
Normal file
28
src/api/user.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { PatchUserRequest, User } from "@root/model/user";
|
||||
import { authStore } from "@root/stores/makeRequest";
|
||||
|
||||
|
||||
const apiUrl = process.env.NODE_ENV === "production" ? "" : "https://hub.pena.digital";
|
||||
|
||||
const makeRequest = authStore.getState().makeRequest;
|
||||
|
||||
export function getUser(userId: string): Promise<User | null> {
|
||||
return makeRequest<never, User>({
|
||||
url: `${apiUrl}/user/${userId}`,
|
||||
contentType: true,
|
||||
method: "GET",
|
||||
useToken: false,
|
||||
withCredentials: false,
|
||||
});
|
||||
}
|
||||
|
||||
export function patchUser(user: PatchUserRequest) {
|
||||
return makeRequest<PatchUserRequest, User>({
|
||||
url: apiUrl + "/user/",
|
||||
contentType: true,
|
||||
method: "PATCH",
|
||||
useToken: true,
|
||||
withCredentials: false,
|
||||
body: user,
|
||||
});
|
||||
}
|
@ -2,43 +2,45 @@ import { Typography, useTheme } from "@mui/material";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
interface Props {
|
||||
text1: string;
|
||||
text2: string;
|
||||
text1: string;
|
||||
text2?: string;
|
||||
}
|
||||
|
||||
export default function ComplexNavText({ text1, text2 }: Props) {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<Typography component="div" sx={{ display: "flex" }}>
|
||||
<Typography
|
||||
component="div"
|
||||
onClick={() => navigate("/tariffs")}
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
fontWeight: 400,
|
||||
fontSize: "12px",
|
||||
lineHeight: "14px",
|
||||
color: theme.palette.grey2.main,
|
||||
}}
|
||||
>
|
||||
{text1}
|
||||
</Typography>
|
||||
<Typography
|
||||
component="span"
|
||||
sx={{
|
||||
cursor: "default",
|
||||
fontWeight: 400,
|
||||
fontSize: "12px",
|
||||
lineHeight: "14px",
|
||||
color: theme.palette.fadePurple.main,
|
||||
textUnderlinePosition: "under",
|
||||
textDecorationColor: theme.palette.brightPurple.main,
|
||||
}}
|
||||
>
|
||||
{text2}
|
||||
</Typography>
|
||||
</Typography>
|
||||
);
|
||||
return (
|
||||
<Typography component="div" sx={{ display: "flex" }}>
|
||||
<Typography
|
||||
component="div"
|
||||
onClick={() => navigate("/tariffs")}
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
fontWeight: 400,
|
||||
fontSize: "12px",
|
||||
lineHeight: "14px",
|
||||
color: theme.palette.grey2.main,
|
||||
}}
|
||||
>
|
||||
{text1}
|
||||
</Typography>
|
||||
{text2 &&
|
||||
<Typography
|
||||
component="span"
|
||||
sx={{
|
||||
cursor: "default",
|
||||
fontWeight: 400,
|
||||
fontSize: "12px",
|
||||
lineHeight: "14px",
|
||||
color: theme.palette.fadePurple.main,
|
||||
textUnderlinePosition: "under",
|
||||
textDecorationColor: theme.palette.brightPurple.main,
|
||||
}}
|
||||
>
|
||||
{text2}
|
||||
</Typography>
|
||||
}
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
|
45
src/components/UnderlinedButtonWithIcon.tsx
Normal file
45
src/components/UnderlinedButtonWithIcon.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
import { Button, ButtonProps, SxProps, Theme, useMediaQuery, useTheme } from "@mui/material";
|
||||
import { MouseEventHandler, ReactNode } from "react";
|
||||
|
||||
|
||||
interface Props {
|
||||
icon?: ReactNode;
|
||||
ButtonProps?: ButtonProps;
|
||||
children?: ReactNode;
|
||||
sx?: SxProps<Theme>;
|
||||
onClick?: MouseEventHandler<HTMLButtonElement>;
|
||||
}
|
||||
|
||||
export default function UnderlinedButtonWithIcon({ ButtonProps, icon, children, sx, onClick }: Props) {
|
||||
const theme = useTheme();
|
||||
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="text"
|
||||
startIcon={icon}
|
||||
disableTouchRipple
|
||||
sx={{
|
||||
p: 0,
|
||||
fontWeight: 400,
|
||||
fontSize: upMd ? "18px" : "16px",
|
||||
lineHeight: "21px",
|
||||
textDecorationLine: "underline",
|
||||
color: "#7E2AEA",
|
||||
textAlign: "start",
|
||||
textUnderlineOffset: "2px",
|
||||
"& .MuiButton-startIcon": {
|
||||
alignSelf: "start",
|
||||
},
|
||||
"&:hover": {
|
||||
backgroundColor: "rgb(0 0 0 / 0)",
|
||||
},
|
||||
...sx,
|
||||
}}
|
||||
onClick={onClick}
|
||||
{...ButtonProps}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
);
|
||||
}
|
21
src/components/icons/CloseSmallIcon.tsx
Normal file
21
src/components/icons/CloseSmallIcon.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import { Box } from "@mui/material";
|
||||
|
||||
|
||||
export default function CloseSmallIcon() {
|
||||
|
||||
return (
|
||||
<Box sx={{
|
||||
width: "24px",
|
||||
height: "24px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
flexShrink: 0,
|
||||
}}>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18 6L6 18" stroke="#A9AAB1" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M6 6L18 18" stroke="#A9AAB1" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</Box>
|
||||
);
|
||||
}
|
21
src/components/icons/EyeIcon.tsx
Normal file
21
src/components/icons/EyeIcon.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import { Box } from "@mui/material";
|
||||
|
||||
|
||||
export default function EyeIcon() {
|
||||
|
||||
return (
|
||||
<Box sx={{
|
||||
width: "24px",
|
||||
height: "24px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
flexShrink: 0,
|
||||
}}>
|
||||
<svg width="22" height="15" viewBox="0 0 22 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.9502 14C16.3669 14 19.6169 9.66667 20.7002 7.5C19.6169 5.33333 16.3669 1 10.9502 1C5.53353 1 2.28353 5.33333 1.2002 7.5C2.64464 9.66667 5.53353 14 10.9502 14Z" stroke="#7E2AEA" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<circle cx="10.9495" cy="7.50033" r="3.58333" stroke="#7E2AEA" strokeWidth="1.5" />
|
||||
</svg>
|
||||
</Box>
|
||||
);
|
||||
}
|
24
src/components/icons/PaperClipIcon.tsx
Normal file
24
src/components/icons/PaperClipIcon.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import { Box } from "@mui/material";
|
||||
|
||||
|
||||
interface Props {
|
||||
color?: string;
|
||||
}
|
||||
|
||||
export default function PaperClipIcon({ color = "#7E2AEA" }: Props) {
|
||||
|
||||
return (
|
||||
<Box sx={{
|
||||
width: "24px",
|
||||
height: "24px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
flexShrink: 0,
|
||||
}}>
|
||||
<svg width="17" height="19" viewBox="0 0 17 19" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15.7541 9.80636C13.714 11.8464 9.42986 16.1306 8.61382 16.9467C7.59378 17.9667 5.14568 19.0548 3.10559 17.0147C1.0655 14.9746 1.47352 12.5265 2.83358 11.1664C4.19364 9.80636 10.9939 3.00608 11.674 2.32605C12.694 1.30601 14.1901 1.44201 15.2101 2.46205C16.2301 3.4821 16.5686 4.91167 15.4141 6.06621C14.0541 7.42626 7.45777 14.1585 6.77775 14.8386C6.09772 15.5186 5.31107 15.276 4.90767 14.8726C4.50426 14.4692 4.26164 13.6825 4.94167 13.0025C5.48569 12.4585 9.79254 8.15163 11.946 5.9982" stroke={color} strokeWidth="1.5" strokeLinecap="round" />
|
||||
</svg>
|
||||
</Box>
|
||||
);
|
||||
}
|
22
src/components/icons/UploadIcon.tsx
Normal file
22
src/components/icons/UploadIcon.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import { Box } from "@mui/material";
|
||||
|
||||
|
||||
export default function UploadIcon() {
|
||||
|
||||
return (
|
||||
<Box sx={{
|
||||
width: "24px",
|
||||
height: "24px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
flexShrink: 0,
|
||||
}}>
|
||||
<svg width="24" height="25" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.0625 7.80957L12 3.80957L15.9375 7.80957" stroke="#7E2AEA" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M12 14.4762V3.80957" stroke="#7E2AEA" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M20.25 14.4761V19.8094C20.25 20.0115 20.171 20.2053 20.0303 20.3482C19.8897 20.491 19.6989 20.5713 19.5 20.5713H4.5C4.30109 20.5713 4.11032 20.491 3.96967 20.3482C3.82902 20.2053 3.75 20.0115 3.75 19.8094V14.4761" stroke="#7E2AEA" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</Box>
|
||||
);
|
||||
}
|
@ -6,7 +6,7 @@ import Faq from "./pages/Faq/Faq";
|
||||
import Wallet from "./pages/Wallet";
|
||||
import Payment from "./pages/Payment/Payment";
|
||||
import Support from "./pages/Support/Support";
|
||||
import AccountSetup from "./pages/AccountSetup";
|
||||
import AccountSetup from "./pages/AccountSetup/AccountSetup";
|
||||
import Landing from "./pages/Landing/Landing";
|
||||
import Tariffs from "./pages/Tariffs/Tariffs";
|
||||
import SigninDialog from "./pages/auth/Signin";
|
||||
@ -20,7 +20,7 @@ import reportWebVitals from "./reportWebVitals";
|
||||
import { SnackbarProvider, enqueueSnackbar } from "notistack";
|
||||
import "./index.css";
|
||||
import Layout from "./components/Layout";
|
||||
import { getUser } from "./api/auth";
|
||||
import { getUser } from "./api/user";
|
||||
import { setUser, useUserStore } from "./stores/user";
|
||||
import TariffConstructor from "./pages/TariffConstructor/TariffConstructor";
|
||||
|
||||
|
@ -1,5 +1,3 @@
|
||||
|
||||
|
||||
export interface RegisterRequest {
|
||||
login: string;
|
||||
password: string;
|
||||
@ -21,14 +19,3 @@ export interface LoginRequest {
|
||||
}
|
||||
|
||||
export type LoginResponse = RegisterResponse;
|
||||
|
||||
export interface User {
|
||||
_id: string;
|
||||
login: string;
|
||||
email: string;
|
||||
phoneNumber: string;
|
||||
isDeleted: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
deletedAt?: string;
|
||||
}
|
44
src/model/user.ts
Normal file
44
src/model/user.ts
Normal file
@ -0,0 +1,44 @@
|
||||
export interface User {
|
||||
_id: string;
|
||||
login: string;
|
||||
email: string;
|
||||
phoneNumber: string;
|
||||
isDeleted: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
deletedAt?: string;
|
||||
}
|
||||
|
||||
export type UserWithFields = User & Partial<Record<UserSettingsField, string>>;
|
||||
|
||||
export type UserSettingsField =
|
||||
| "password"
|
||||
| "email"
|
||||
| "phoneNumber"
|
||||
| "name"
|
||||
| "surname"
|
||||
| "middleName"
|
||||
| "companyName";
|
||||
|
||||
export type UserSettings = Record<UserSettingsField, {
|
||||
value: string;
|
||||
error: string | null;
|
||||
touched: boolean;
|
||||
}>;
|
||||
|
||||
export type PatchUserRequest = Partial<Record<UserSettingsField, string>>;
|
||||
|
||||
export type VerificationStatus = "verificated" | "notVerificated" | "waiting";
|
||||
|
||||
export type UserDocument = {
|
||||
file: File | null;
|
||||
uploadedFileName: string | null;
|
||||
imageSrc: string | null;
|
||||
};
|
||||
|
||||
export type UserDocumentTypes =
|
||||
| "ИНН"
|
||||
| "Устав"
|
||||
| "Свидетельство о регистрации НКО";
|
||||
|
||||
export type UserDocuments = Record<UserDocumentTypes, UserDocument>;
|
@ -1,272 +0,0 @@
|
||||
import { Box, Button, Link, Typography, useMediaQuery, useTheme } from "@mui/material";
|
||||
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";
|
||||
import { useState } from "react";
|
||||
|
||||
export default function AccountSetup() {
|
||||
const theme = useTheme();
|
||||
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
||||
|
||||
const [name, setName] = useState<string>("");
|
||||
const [email, setEmail] = useState<string>("");
|
||||
const [surname, setSurname] = useState<string>("");
|
||||
const [telephone, setTelephone] = useState<string>("");
|
||||
const [otchestvo, setOtchestvo] = useState<string>("");
|
||||
const [password, setPassword] = useState<string>("");
|
||||
const [сompanyName, setCompanyName] = useState<string>("");
|
||||
|
||||
const [avatar, setAvatar] = useState<any>();
|
||||
|
||||
const imgHC = (imgInp: any) => {
|
||||
if (imgInp.target.files !== null) {
|
||||
const file = imgInp.target.files[0];
|
||||
setAvatar(URL.createObjectURL(file));
|
||||
handleClose();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionWrapper
|
||||
maxWidth="lg"
|
||||
sx={{
|
||||
mt: "25px",
|
||||
mb: "70px",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
component="div"
|
||||
sx={{ fontWeight: "400px", fontSize: "12px", lineHeight: "14px", marginBottom: "19px" }}
|
||||
>
|
||||
Настройки аккаунта
|
||||
</Typography>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
mt: "20px",
|
||||
mb: "40px",
|
||||
display: "flex",
|
||||
gap: "10px",
|
||||
}}
|
||||
>
|
||||
<Typography variant="h4">Настройки аккаунта</Typography>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
backgroundColor: "white",
|
||||
display: "flex",
|
||||
flexDirection: upMd ? "row" : "column",
|
||||
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={{ display: "flex", justifyContent: "left", ml: "20px", mt: "20px", mr: "58px" }}>
|
||||
<Typography component="div">
|
||||
<Box component="image" sx={{ display: "flex", width: "220px", height: "220px", borderRadius: "8px" }}>
|
||||
<img src={avatar ? avatar : Account} alt="Account" />
|
||||
</Box>
|
||||
<Button
|
||||
variant="outlined"
|
||||
component="label"
|
||||
sx={{
|
||||
width: "220px",
|
||||
paddingTop: "10px",
|
||||
paddingBottom: "10px",
|
||||
borderRadius: "8px",
|
||||
boxShadow: "none",
|
||||
|
||||
color: theme.palette.brightPurple.main,
|
||||
borderColor: theme.palette.brightPurple.main,
|
||||
mt: "30px",
|
||||
}}
|
||||
>
|
||||
Загрузить фото
|
||||
<input type="file" hidden onChange={(event) => imgHC(event)} />
|
||||
</Button>
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
mt: "10px",
|
||||
mb: "10px",
|
||||
mr: "20px",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
component="div"
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
flexWrap: "wrap",
|
||||
columnGap: "30px",
|
||||
ml: "20px",
|
||||
mt: "20px",
|
||||
}}
|
||||
>
|
||||
<InputTextfield
|
||||
TextfieldProps={{
|
||||
placeholder: "Имя",
|
||||
}}
|
||||
onChange={(event) => setName(event.target.value)}
|
||||
id="text"
|
||||
label="Имя"
|
||||
gap={upMd ? "15px" : "10px"}
|
||||
color={name ? "#e8badd" : ""}
|
||||
FormInputSx={{ width: upMd ? "45%" : "100%", mt: "19px", maxWidth: "406px" }}
|
||||
/>
|
||||
|
||||
<InputTextfield
|
||||
TextfieldProps={{
|
||||
placeholder: "username@penahaub.com",
|
||||
|
||||
type: "text",
|
||||
}}
|
||||
onChange={(event) => setEmail(event.target.value)}
|
||||
id="email"
|
||||
label="E-mail"
|
||||
gap={upMd ? "15px" : "10px"}
|
||||
FormInputSx={{ width: upMd ? "45%" : "100%", mt: "19px", maxWidth: "406px" }}
|
||||
color={email ? "#e8badd" : ""}
|
||||
/>
|
||||
|
||||
<InputTextfield
|
||||
TextfieldProps={{
|
||||
placeholder: "Фамилия",
|
||||
|
||||
type: "text",
|
||||
}}
|
||||
onChange={(event) => setSurname(event.target.value)}
|
||||
id="password"
|
||||
label="Фамилия"
|
||||
gap={upMd ? "15px" : "10px"}
|
||||
FormInputSx={{ width: upMd ? "45%" : "100%", mt: "19px", maxWidth: "406px" }}
|
||||
color={surname ? "#e8badd" : ""}
|
||||
/>
|
||||
|
||||
<InputTextfield
|
||||
TextfieldProps={{
|
||||
placeholder: "+7 900 000 00 00",
|
||||
type: "text",
|
||||
}}
|
||||
onChange={(enent) => setTelephone(enent.target.value)}
|
||||
id="password"
|
||||
label="Телефон"
|
||||
gap={upMd ? "15px" : "10px"}
|
||||
FormInputSx={{ width: upMd ? "45%" : "100%", mt: "19px", maxWidth: "406px" }}
|
||||
color={telephone ? "#e8badd" : ""}
|
||||
/>
|
||||
|
||||
<InputTextfield
|
||||
TextfieldProps={{
|
||||
placeholder: "Отчество",
|
||||
|
||||
type: "text",
|
||||
}}
|
||||
onChange={(enent) => setOtchestvo(enent.target.value)}
|
||||
id="password"
|
||||
label="Отчество"
|
||||
gap={upMd ? "15px" : "10px"}
|
||||
FormInputSx={{ width: upMd ? "45%" : "100%", mt: "19px", maxWidth: "406px" }}
|
||||
color={otchestvo ? "#e8badd" : ""}
|
||||
/>
|
||||
|
||||
<InputTextfield
|
||||
TextfieldProps={{
|
||||
placeholder: "Не мение 8 символов",
|
||||
type: "password",
|
||||
}}
|
||||
onChange={(enent) => setPassword(enent.target.value)}
|
||||
id="email"
|
||||
label="Пароль"
|
||||
gap={upMd ? "15px" : "10px"}
|
||||
FormInputSx={{ width: upMd ? "45%" : "100%", mt: "19px", maxWidth: "406px" }}
|
||||
color={password ? "#e8badd" : ""}
|
||||
/>
|
||||
|
||||
<InputTextfield
|
||||
TextfieldProps={{
|
||||
placeholder: "ООО Фирма",
|
||||
type: "text",
|
||||
}}
|
||||
onChange={(enent) => setCompanyName(enent.target.value)}
|
||||
id="text"
|
||||
label="Название компании"
|
||||
gap={upMd ? "15px" : "10px"}
|
||||
FormInputSx={{ width: upMd ? "45%" : "100%", mt: "19px", maxWidth: "406px" }}
|
||||
color={сompanyName ? " #e8badd" : ""}
|
||||
/>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
margin: 0,
|
||||
width: upMd ? "45%" : "100%",
|
||||
padding: 0,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
maxWidth: "406px",
|
||||
}}
|
||||
>
|
||||
<Typography sx={{ display: "flex", fontSize: upMd ? "16px" : "18px" }}>
|
||||
<img src={Download} alt="Download" />
|
||||
<Link
|
||||
href="#"
|
||||
sx={{
|
||||
color: theme.palette.brightPurple.main,
|
||||
marginLeft: "11px",
|
||||
}}
|
||||
>
|
||||
Загрузить документы для юр лиц
|
||||
</Link>
|
||||
</Typography>
|
||||
|
||||
<Typography sx={{ display: "flex", fontSize: upMd ? "16px" : "18px" }}>
|
||||
<img src={Download} alt="Download" />
|
||||
<Link
|
||||
href="#"
|
||||
sx={{
|
||||
color: theme.palette.brightPurple.main,
|
||||
marginLeft: "11px",
|
||||
marginTop: "5px",
|
||||
}}
|
||||
>
|
||||
Загрузить документы для НКО
|
||||
</Link>
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography component="div" sx={{ width: "100%", display: "flex", justifyContent: "right" }}>
|
||||
<CustomButton
|
||||
variant="contained"
|
||||
sx={{
|
||||
width: "180px",
|
||||
height: "44px",
|
||||
backgroundColor: theme.palette.brightPurple.main,
|
||||
textColor: "white",
|
||||
mb: "20px",
|
||||
}}
|
||||
>
|
||||
Cохранить
|
||||
</CustomButton>
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</SectionWrapper>
|
||||
);
|
||||
}
|
||||
function handleClose() {
|
||||
throw new Error("Function not implemented.");
|
||||
}
|
246
src/pages/AccountSetup/AccountSetup.tsx
Normal file
246
src/pages/AccountSetup/AccountSetup.tsx
Normal file
@ -0,0 +1,246 @@
|
||||
import { Box, SxProps, Theme, Typography, useMediaQuery, useTheme } from "@mui/material";
|
||||
import CustomButton from "@components/CustomButton";
|
||||
import InputTextfield from "@components/InputTextfield";
|
||||
import SectionWrapper from "@components/SectionWrapper";
|
||||
import ComplexNavText from "@root/components/ComplexNavText";
|
||||
import { openDocumentsDialog, sendUserData, setSettingsField, useUserStore } from "@root/stores/user";
|
||||
import UnderlinedButtonWithIcon from "@root/components/UnderlinedButtonWithIcon";
|
||||
import UploadIcon from "@root/components/icons/UploadIcon";
|
||||
import { VerificationStatus } from "@root/model/user";
|
||||
import DocumentsDialog from "./DocumentsDialog/DocumentsDialog";
|
||||
import EyeIcon from "@root/components/icons/EyeIcon";
|
||||
|
||||
|
||||
export default function AccountSetup() {
|
||||
const theme = useTheme();
|
||||
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
||||
const upSm = useMediaQuery(theme.breakpoints.up("sm"));
|
||||
const fields = useUserStore(state => state.settingsFields);
|
||||
const verificationStatus = useUserStore(state => state.verificationStatus);
|
||||
const verificationType = useUserStore(state => state.verificationType);
|
||||
|
||||
const textFieldProps = {
|
||||
gap: upMd ? "16px" : "10px",
|
||||
color: "#F2F3F7",
|
||||
bold: true,
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionWrapper
|
||||
maxWidth="lg"
|
||||
sx={{
|
||||
mt: "25px",
|
||||
mb: "70px",
|
||||
}}
|
||||
>
|
||||
<DocumentsDialog />
|
||||
<ComplexNavText text1="Настройки аккаунта" />
|
||||
<Typography variant="h4" mt="20px">Настройки аккаунта</Typography>
|
||||
<Box sx={{
|
||||
mt: "40px",
|
||||
mb: "40px",
|
||||
backgroundColor: "white",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
borderRadius: "12px",
|
||||
p: "20px",
|
||||
gap: "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={{
|
||||
display: "flex",
|
||||
gap: "31px",
|
||||
justifyContent: "space-between",
|
||||
flexDirection: upMd ? "row" : "column",
|
||||
}}>
|
||||
<Box sx={{
|
||||
display: "grid",
|
||||
gridAutoFlow: upSm ? "column" : "row",
|
||||
gridTemplateRows: "repeat(4, auto)",
|
||||
rowGap: "15px",
|
||||
columnGap: "31px",
|
||||
flexGrow: 1,
|
||||
}}>
|
||||
<InputTextfield
|
||||
TextfieldProps={{
|
||||
placeholder: "Имя",
|
||||
value: fields?.name.value || "",
|
||||
helperText: fields?.name.touched && fields.name.error,
|
||||
error: fields?.name.touched && Boolean(fields.name.error),
|
||||
}}
|
||||
onChange={e => setSettingsField("name", e.target.value)}
|
||||
id="name"
|
||||
label="Имя"
|
||||
{...textFieldProps}
|
||||
/>
|
||||
<InputTextfield
|
||||
TextfieldProps={{
|
||||
placeholder: "Фамилия",
|
||||
value: fields?.surname.value || "",
|
||||
helperText: fields?.surname.touched && fields.surname.error,
|
||||
error: fields?.surname.touched && Boolean(fields.surname.error),
|
||||
}}
|
||||
onChange={e => setSettingsField("surname", e.target.value)}
|
||||
id="surname"
|
||||
label="Фамилия"
|
||||
{...textFieldProps}
|
||||
/>
|
||||
<InputTextfield
|
||||
TextfieldProps={{
|
||||
placeholder: "Отчество",
|
||||
value: fields?.middleName.value || "",
|
||||
helperText: fields?.middleName.touched && fields.middleName.error,
|
||||
error: fields?.middleName.touched && Boolean(fields.middleName.error),
|
||||
}}
|
||||
onChange={e => setSettingsField("middleName", e.target.value)}
|
||||
id="middleName"
|
||||
label="Отчество"
|
||||
{...textFieldProps}
|
||||
/>
|
||||
<InputTextfield
|
||||
TextfieldProps={{
|
||||
placeholder: "ООО Фирма",
|
||||
value: fields?.companyName.value || "",
|
||||
helperText: fields?.companyName.touched && fields.companyName.error,
|
||||
error: fields?.companyName.touched && Boolean(fields.companyName.error),
|
||||
}}
|
||||
onChange={e => setSettingsField("companyName", e.target.value)}
|
||||
id="companyName"
|
||||
label="Название компании"
|
||||
{...textFieldProps}
|
||||
/>
|
||||
<InputTextfield
|
||||
TextfieldProps={{
|
||||
placeholder: "username@penahaub.com",
|
||||
value: fields?.email.value || "",
|
||||
helperText: fields?.email.touched && fields.email.error,
|
||||
error: fields?.email.touched && Boolean(fields.email.error),
|
||||
}}
|
||||
onChange={e => setSettingsField("email", e.target.value)}
|
||||
id="email"
|
||||
label="E-mail"
|
||||
{...textFieldProps}
|
||||
/>
|
||||
<InputTextfield
|
||||
TextfieldProps={{
|
||||
placeholder: "+7 900 000 00 00",
|
||||
value: fields?.phoneNumber.value || "",
|
||||
helperText: fields?.phoneNumber.touched && fields.phoneNumber.error,
|
||||
error: fields?.phoneNumber.touched && Boolean(fields.phoneNumber.error),
|
||||
}}
|
||||
onChange={e => setSettingsField("phoneNumber", e.target.value)}
|
||||
id="phoneNumber"
|
||||
label="Телефон"
|
||||
{...textFieldProps}
|
||||
/>
|
||||
<InputTextfield
|
||||
TextfieldProps={{
|
||||
placeholder: "Не менее 8 символов",
|
||||
value: fields?.password.value || "",
|
||||
helperText: fields?.password.touched && fields.password.error,
|
||||
error: fields?.password.touched && Boolean(fields.password.error),
|
||||
type: "password",
|
||||
}}
|
||||
onChange={e => setSettingsField("password", e.target.value)}
|
||||
id="password"
|
||||
label="Пароль"
|
||||
{...textFieldProps}
|
||||
/>
|
||||
</Box>
|
||||
<Box sx={{
|
||||
maxWidth: "246px",
|
||||
}}>
|
||||
<Typography variant="p1">Статус</Typography>
|
||||
<VerificationIndicator
|
||||
verificationStatus={verificationStatus}
|
||||
sx={{ mt: "16px" }}
|
||||
/>
|
||||
{verificationStatus === "notVerificated" &&
|
||||
<>
|
||||
<UnderlinedButtonWithIcon
|
||||
icon={<UploadIcon />}
|
||||
sx={{ mt: "55px" }}
|
||||
ButtonProps={{
|
||||
onClick: () => openDocumentsDialog("juridical"),
|
||||
}}
|
||||
>Загрузить документы для юр лиц</UnderlinedButtonWithIcon>
|
||||
<UnderlinedButtonWithIcon
|
||||
icon={<UploadIcon />}
|
||||
sx={{ mt: "15px" }}
|
||||
ButtonProps={{
|
||||
onClick: () => openDocumentsDialog("nko"),
|
||||
}}
|
||||
>Загрузить документы для НКО</UnderlinedButtonWithIcon>
|
||||
</>
|
||||
}
|
||||
{verificationStatus === "verificated" &&
|
||||
<UnderlinedButtonWithIcon
|
||||
icon={<EyeIcon />}
|
||||
sx={{ mt: "55px" }}
|
||||
ButtonProps={{
|
||||
onClick: () => openDocumentsDialog(verificationType),
|
||||
}}
|
||||
>Посмотреть свою верификацию</UnderlinedButtonWithIcon>
|
||||
}
|
||||
</Box>
|
||||
</Box>
|
||||
<CustomButton
|
||||
onClick={sendUserData}
|
||||
variant="contained"
|
||||
sx={{
|
||||
width: "180px",
|
||||
height: "44px",
|
||||
alignSelf: "end",
|
||||
backgroundColor: theme.palette.brightPurple.main,
|
||||
textColor: "white",
|
||||
}}
|
||||
>
|
||||
Сохранить
|
||||
</CustomButton>
|
||||
</Box>
|
||||
</SectionWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
const verificationStatusData: Record<VerificationStatus, { text: string; color: string; }> = {
|
||||
"verificated": {
|
||||
text: "Верификация пройдена",
|
||||
color: "#0D9F00",
|
||||
},
|
||||
"waiting": {
|
||||
text: "В ожидании верификации",
|
||||
color: "#F18956",
|
||||
},
|
||||
"notVerificated": {
|
||||
text: "Не верифицирован",
|
||||
color: "#E02C2C",
|
||||
},
|
||||
};
|
||||
|
||||
function VerificationIndicator({ verificationStatus, sx }: {
|
||||
verificationStatus: VerificationStatus;
|
||||
sx?: SxProps<Theme>;
|
||||
}) {
|
||||
return (
|
||||
<Box sx={{
|
||||
py: "14px",
|
||||
px: "8.5px",
|
||||
borderWidth: "1px",
|
||||
borderStyle: "solid",
|
||||
color: verificationStatusData[verificationStatus].color,
|
||||
borderColor: verificationStatusData[verificationStatus].color,
|
||||
borderRadius: "8px",
|
||||
textAlign: "center",
|
||||
...sx,
|
||||
}}>
|
||||
<Typography lineHeight="100%">{verificationStatusData[verificationStatus].text}</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
45
src/pages/AccountSetup/DocumentsDialog/DocumentItem.tsx
Normal file
45
src/pages/AccountSetup/DocumentsDialog/DocumentItem.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
import { Box, SxProps, Theme, Typography, useTheme } from "@mui/material";
|
||||
import { UserDocument } from "@root/model/user";
|
||||
|
||||
|
||||
interface Props {
|
||||
text: string;
|
||||
document: UserDocument;
|
||||
sx?: SxProps<Theme>;
|
||||
}
|
||||
|
||||
export default function DocumentItem({ text, document, sx }: Props) {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Box sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "start",
|
||||
gap: "10px",
|
||||
...sx,
|
||||
}}>
|
||||
<Typography sx={{
|
||||
color: "#4D4D4D",
|
||||
fontWeight: 500,
|
||||
fontVariantNumeric: "tabular-nums",
|
||||
}}>{text}</Typography>
|
||||
{document.uploadedFileName &&
|
||||
<Typography sx={{
|
||||
color: theme.palette.brightPurple.main,
|
||||
}}>{document.uploadedFileName}</Typography>
|
||||
}
|
||||
{document.imageSrc &&
|
||||
<img
|
||||
src={document.imageSrc}
|
||||
alt="document"
|
||||
style={{
|
||||
maxWidth: "80px",
|
||||
maxHeight: "200px",
|
||||
objectFit: "contain",
|
||||
display: "block",
|
||||
}}
|
||||
/>}
|
||||
</Box>
|
||||
);
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
import { Box, SxProps, Theme, Typography } from "@mui/material";
|
||||
import UnderlinedButtonWithIcon from "@root/components/UnderlinedButtonWithIcon";
|
||||
import PaperClipIcon from "@root/components/icons/PaperClipIcon";
|
||||
import { UserDocument } from "@root/model/user";
|
||||
import { ChangeEvent, useRef } from "react";
|
||||
|
||||
|
||||
interface Props {
|
||||
text: string;
|
||||
document: UserDocument;
|
||||
onFileChange: (event: ChangeEvent<HTMLInputElement>) => void;
|
||||
sx?: SxProps<Theme>;
|
||||
}
|
||||
|
||||
export default function DocumentUploadItem({ text, document, onFileChange, sx }: Props) {
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
function handleChooseFileClick() {
|
||||
fileInputRef.current?.click();
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "start",
|
||||
gap: "10px",
|
||||
...sx,
|
||||
}}>
|
||||
<Typography sx={{
|
||||
color: "#4D4D4D",
|
||||
fontWeight: 500,
|
||||
fontVariantNumeric: "tabular-nums",
|
||||
}}>{text}</Typography>
|
||||
<UnderlinedButtonWithIcon
|
||||
icon={<PaperClipIcon />}
|
||||
onClick={handleChooseFileClick}
|
||||
>{document.file ? document.file.name : "Выберите файл"}</UnderlinedButtonWithIcon>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
style={{ display: "none" }}
|
||||
onChange={onFileChange}
|
||||
type="file"
|
||||
id="image-file"
|
||||
multiple
|
||||
accept="image/*"
|
||||
/>
|
||||
{document.imageSrc &&
|
||||
<img
|
||||
src={document.imageSrc}
|
||||
alt="document"
|
||||
style={{
|
||||
maxWidth: "80px",
|
||||
maxHeight: "200px",
|
||||
objectFit: "contain",
|
||||
display: "block",
|
||||
}}
|
||||
/>
|
||||
}
|
||||
</Box>
|
||||
);
|
||||
}
|
10
src/pages/AccountSetup/DocumentsDialog/DocumentsDialog.tsx
Normal file
10
src/pages/AccountSetup/DocumentsDialog/DocumentsDialog.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import { useUserStore } from "@root/stores/user";
|
||||
import NkoDocumentsDialog from "./NkoDocumentsDialog";
|
||||
import JuridicalDocumentsDialog from "./JuridicalDocumentsDialog";
|
||||
|
||||
|
||||
export default function DocumentsDialog() {
|
||||
const type = useUserStore(state => state.dialogType);
|
||||
|
||||
return type === "juridical" ? <JuridicalDocumentsDialog /> : <NkoDocumentsDialog />
|
||||
}
|
@ -0,0 +1,107 @@
|
||||
import { Box, Dialog, IconButton, Typography, useTheme } from "@mui/material";
|
||||
import CustomButton from "@root/components/CustomButton";
|
||||
import CloseSmallIcon from "@root/components/icons/CloseSmallIcon";
|
||||
import { closeDocumentsDialog, sendDocuments, setDocument, useUserStore } from "@root/stores/user";
|
||||
import DocumentUploadItem from "./DocumentUploadItem";
|
||||
import DocumentItem from "./DocumentItem";
|
||||
|
||||
|
||||
export default function JuridicalDocumentsDialog() {
|
||||
const theme = useTheme();
|
||||
const isOpen = useUserStore(state => state.isDocumentsDialogOpen);
|
||||
const verificationStatus = useUserStore(state => state.verificationStatus);
|
||||
const documents = useUserStore(state => state.documents);
|
||||
|
||||
const documentElements = verificationStatus === "verificated" ? (
|
||||
<>
|
||||
<DocumentItem
|
||||
text="1. Скан ИНН организации НКО (выписка из ЕГЮРЛ)"
|
||||
document={documents["ИНН"]}
|
||||
/>
|
||||
<DocumentItem
|
||||
text="2. Устав организации"
|
||||
document={documents["Устав"]}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<DocumentUploadItem
|
||||
document={documents["ИНН"]}
|
||||
text="1. Скан ИНН организации (выписка из ЕГЮРЛ)"
|
||||
onFileChange={e => setDocument("ИНН", e.target?.files?.[0])}
|
||||
/>
|
||||
<DocumentUploadItem
|
||||
document={documents["Устав"]}
|
||||
text="2. Устав организации"
|
||||
onFileChange={e => setDocument("Устав", e.target?.files?.[0])}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={isOpen}
|
||||
onClose={closeDocumentsDialog}
|
||||
PaperProps={{
|
||||
sx: {
|
||||
width: "600px",
|
||||
maxWidth: "600px",
|
||||
backgroundColor: "white",
|
||||
position: "relative",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
p: "20px",
|
||||
gap: "20px",
|
||||
borderRadius: "12px",
|
||||
boxShadow: "none",
|
||||
}
|
||||
}}
|
||||
slotProps={{ backdrop: { style: { backgroundColor: "rgb(0 0 0 / 0.7)" } } }}
|
||||
>
|
||||
<IconButton
|
||||
onClick={closeDocumentsDialog}
|
||||
sx={{
|
||||
position: "absolute",
|
||||
right: "7px",
|
||||
top: "7px",
|
||||
}}
|
||||
>
|
||||
<CloseSmallIcon />
|
||||
</IconButton>
|
||||
<Box sx={{
|
||||
p: "20px",
|
||||
}}>
|
||||
<Typography variant="h5" lineHeight="100%">
|
||||
{verificationStatus === "verificated" ? "Ваши документы" : "Загрузите документы"}
|
||||
</Typography>
|
||||
<Typography sx={{
|
||||
fontWeight: 400,
|
||||
fontSize: "16px",
|
||||
lineHeight: "100%",
|
||||
mt: "12px",
|
||||
}}>для верификации юридических лиц в формате PDF</Typography>
|
||||
<Box sx={{
|
||||
mt: "30px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "25px",
|
||||
}}>
|
||||
{documentElements}
|
||||
</Box>
|
||||
</Box>
|
||||
<CustomButton
|
||||
onClick={sendDocuments}
|
||||
variant="contained"
|
||||
sx={{
|
||||
width: "180px",
|
||||
height: "44px",
|
||||
alignSelf: "end",
|
||||
backgroundColor: theme.palette.brightPurple.main,
|
||||
textColor: "white",
|
||||
}}
|
||||
>
|
||||
Отправить
|
||||
</CustomButton>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
116
src/pages/AccountSetup/DocumentsDialog/NkoDocumentsDialog.tsx
Normal file
116
src/pages/AccountSetup/DocumentsDialog/NkoDocumentsDialog.tsx
Normal file
@ -0,0 +1,116 @@
|
||||
import { Box, Dialog, IconButton, Typography, useTheme } from "@mui/material";
|
||||
import CustomButton from "@root/components/CustomButton";
|
||||
import CloseSmallIcon from "@root/components/icons/CloseSmallIcon";
|
||||
import { closeDocumentsDialog, sendDocuments, setDocument, useUserStore } from "@root/stores/user";
|
||||
import DocumentUploadItem from "./DocumentUploadItem";
|
||||
import DocumentItem from "./DocumentItem";
|
||||
|
||||
|
||||
export default function NkoDocumentsDialog() {
|
||||
const theme = useTheme();
|
||||
const isOpen = useUserStore(state => state.isDocumentsDialogOpen);
|
||||
const verificationStatus = useUserStore(state => state.verificationStatus);
|
||||
const documents = useUserStore(state => state.documents);
|
||||
|
||||
const documentElements = verificationStatus === "verificated" ? (
|
||||
<>
|
||||
<DocumentItem
|
||||
text="1. Свидетельство о регистрации НКО"
|
||||
document={documents["Свидетельство о регистрации НКО"]}
|
||||
/>
|
||||
<DocumentItem
|
||||
text="2. Скан ИНН организации НКО (выписка из ЕГЮРЛ)"
|
||||
document={documents["ИНН"]}
|
||||
/>
|
||||
<DocumentItem
|
||||
text="3. Устав организации"
|
||||
document={documents["Устав"]}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<DocumentUploadItem
|
||||
text="1. Свидетельство о регистрации НКО"
|
||||
document={documents["Свидетельство о регистрации НКО"]}
|
||||
onFileChange={e => setDocument("Свидетельство о регистрации НКО", e.target?.files?.[0])}
|
||||
/>
|
||||
<DocumentUploadItem
|
||||
text="2. Скан ИНН организации НКО (выписка из ЕГЮРЛ)"
|
||||
document={documents["ИНН"]}
|
||||
onFileChange={e => setDocument("ИНН", e.target?.files?.[0])}
|
||||
/>
|
||||
<DocumentUploadItem
|
||||
text="3. Устав организации"
|
||||
document={documents["Устав"]}
|
||||
onFileChange={e => setDocument("Устав", e.target?.files?.[0])}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={isOpen}
|
||||
onClose={closeDocumentsDialog}
|
||||
PaperProps={{
|
||||
sx: {
|
||||
width: "600px",
|
||||
maxWidth: "600px",
|
||||
backgroundColor: "white",
|
||||
position: "relative",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
p: "20px",
|
||||
gap: "20px",
|
||||
borderRadius: "12px",
|
||||
boxShadow: "none",
|
||||
}
|
||||
}}
|
||||
slotProps={{ backdrop: { style: { backgroundColor: "rgb(0 0 0 / 0.7)" } } }}
|
||||
>
|
||||
<IconButton
|
||||
onClick={closeDocumentsDialog}
|
||||
sx={{
|
||||
position: "absolute",
|
||||
right: "7px",
|
||||
top: "7px",
|
||||
}}
|
||||
>
|
||||
<CloseSmallIcon />
|
||||
</IconButton>
|
||||
<Box sx={{
|
||||
p: "20px",
|
||||
}}>
|
||||
<Typography variant="h5" lineHeight="100%">
|
||||
{verificationStatus === "verificated" ? "Ваши документы" : "Загрузите документы"}
|
||||
</Typography>
|
||||
<Typography sx={{
|
||||
fontWeight: 400,
|
||||
fontSize: "16px",
|
||||
lineHeight: "100%",
|
||||
mt: "12px",
|
||||
}}>для верификации НКО в формате PDF</Typography>
|
||||
<Box sx={{
|
||||
mt: "30px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "25px",
|
||||
}}>
|
||||
{documentElements}
|
||||
</Box>
|
||||
</Box>
|
||||
<CustomButton
|
||||
onClick={sendDocuments}
|
||||
variant="contained"
|
||||
sx={{
|
||||
width: "180px",
|
||||
height: "44px",
|
||||
alignSelf: "end",
|
||||
backgroundColor: theme.palette.brightPurple.main,
|
||||
textColor: "white",
|
||||
}}
|
||||
>
|
||||
Отправить
|
||||
</CustomButton>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
@ -44,8 +44,8 @@ export default function SigninDialog() {
|
||||
makeRequest<LoginRequest, LoginResponse>({
|
||||
url: "https://hub.pena.digital/auth/login",
|
||||
body: {
|
||||
login: values.login,
|
||||
password: values.password,
|
||||
login: values.login.trim(),
|
||||
password: values.password.trim(),
|
||||
},
|
||||
useToken: false,
|
||||
withCredentials: true,
|
||||
@ -76,38 +76,39 @@ export default function SigninDialog() {
|
||||
setTimeout(() => navigate("/"), theme.transitions.duration.leavingScreen);
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={isDialogOpen}
|
||||
onClose={handleClose}
|
||||
PaperProps={{
|
||||
sx: {
|
||||
width: "600px",
|
||||
},
|
||||
}}
|
||||
slotProps={{
|
||||
backdrop: {
|
||||
style: {
|
||||
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: `
|
||||
return (
|
||||
<Dialog
|
||||
open={isDialogOpen}
|
||||
onClose={handleClose}
|
||||
PaperProps={{
|
||||
sx: {
|
||||
width: "600px",
|
||||
maxWidth: "600px",
|
||||
}
|
||||
}}
|
||||
slotProps={{
|
||||
backdrop: {
|
||||
style: {
|
||||
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: `
|
||||
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),
|
||||
|
@ -48,8 +48,8 @@ export default function SignupDialog() {
|
||||
makeRequest<RegisterRequest, RegisterResponse>({
|
||||
url: "https://hub.pena.digital/auth/register",
|
||||
body: {
|
||||
login: values.login,
|
||||
password: values.password,
|
||||
login: values.login.trim(),
|
||||
password: values.password.trim(),
|
||||
phoneNumber: "-",
|
||||
},
|
||||
useToken: false,
|
||||
@ -82,6 +82,7 @@ export default function SignupDialog() {
|
||||
PaperProps={{
|
||||
sx: {
|
||||
width: "600px",
|
||||
maxWidth: "600px",
|
||||
}
|
||||
}}
|
||||
slotProps={{
|
||||
|
@ -14,9 +14,11 @@ interface FirstRequest<T> {
|
||||
method?: string;
|
||||
url: string;
|
||||
body?: T;
|
||||
/** Send access token */
|
||||
useToken?: boolean;
|
||||
contentType?: boolean;
|
||||
signal?: AbortSignal;
|
||||
/** Send refresh token */
|
||||
withCredentials?: boolean;
|
||||
}
|
||||
|
||||
|
@ -1,16 +1,57 @@
|
||||
import { User } from "@root/model/auth";
|
||||
import { PatchUserRequest, UserDocumentTypes, UserDocuments, UserSettings, UserSettingsField, UserWithFields, VerificationStatus } from "@root/model/user";
|
||||
import { produce } from "immer";
|
||||
import { create } from "zustand";
|
||||
import { createJSONStorage, devtools, persist } from "zustand/middleware";
|
||||
import { StringSchema, string } from "yup";
|
||||
import { patchUser } from "@root/api/user";
|
||||
|
||||
|
||||
interface UserStore {
|
||||
userId: string | null;
|
||||
user: User | null;
|
||||
user: UserWithFields | null;
|
||||
settingsFields: UserSettings | null;
|
||||
verificationStatus: VerificationStatus;
|
||||
verificationType: "juridical" | "nko";
|
||||
isDocumentsDialogOpen: boolean;
|
||||
dialogType: "juridical" | "nko";
|
||||
documents: UserDocuments;
|
||||
}
|
||||
|
||||
const defaultFieldValues = {
|
||||
value: "",
|
||||
error: null,
|
||||
touched: false,
|
||||
};
|
||||
|
||||
const defaultFields = {
|
||||
name: { ...defaultFieldValues },
|
||||
surname: { ...defaultFieldValues },
|
||||
middleName: { ...defaultFieldValues },
|
||||
companyName: { ...defaultFieldValues },
|
||||
email: { ...defaultFieldValues },
|
||||
phoneNumber: { ...defaultFieldValues },
|
||||
password: { ...defaultFieldValues },
|
||||
};
|
||||
|
||||
const defaultDocument = {
|
||||
file: null,
|
||||
uploadedFileName: null,
|
||||
imageSrc: null,
|
||||
};
|
||||
|
||||
const initialState: UserStore = {
|
||||
userId: null,
|
||||
user: null,
|
||||
settingsFields: { ...defaultFields },
|
||||
verificationStatus: "notVerificated",
|
||||
verificationType: "juridical",
|
||||
isDocumentsDialogOpen: false,
|
||||
dialogType: "juridical",
|
||||
documents: {
|
||||
"ИНН": { ...defaultDocument },
|
||||
"Устав": { ...defaultDocument },
|
||||
"Свидетельство о регистрации НКО": { ...defaultDocument },
|
||||
}
|
||||
};
|
||||
|
||||
export const useUserStore = create<UserStore>()(
|
||||
@ -19,16 +60,159 @@ export const useUserStore = create<UserStore>()(
|
||||
(set, get) => initialState,
|
||||
{
|
||||
name: "User store",
|
||||
enabled: process.env.NODE_ENV === "development",
|
||||
}
|
||||
),
|
||||
{
|
||||
version: 1,
|
||||
name: "user",
|
||||
storage: createJSONStorage(() => localStorage),
|
||||
partialize: state => ({
|
||||
userId: state.userId,
|
||||
user: state.user,
|
||||
})
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
export const setUserId = (userId: string | null) => useUserStore.setState({ userId });
|
||||
export const setUser = (user: User | null) => useUserStore.setState({ user });
|
||||
|
||||
export const clearUser = () => useUserStore.setState({ ...initialState });
|
||||
export const openDocumentsDialog = (type: UserStore["dialogType"]) => useUserStore.setState(
|
||||
produce<UserStore>(state => {
|
||||
state.isDocumentsDialogOpen = true;
|
||||
state.dialogType = type;
|
||||
})
|
||||
);
|
||||
|
||||
export const closeDocumentsDialog = () => useUserStore.setState(
|
||||
produce<UserStore>(state => {
|
||||
state.isDocumentsDialogOpen = false;
|
||||
})
|
||||
);
|
||||
|
||||
export const setDocument = (type: UserDocumentTypes, file: File | undefined) => useUserStore.setState(
|
||||
produce<UserStore>(state => {
|
||||
if (!file) {
|
||||
state.documents[type] = { ...defaultDocument };
|
||||
return;
|
||||
}
|
||||
|
||||
let imageSrc: string | null = null;
|
||||
|
||||
try {
|
||||
const src = state.documents[type].imageSrc;
|
||||
if (src) URL.revokeObjectURL(src);
|
||||
|
||||
imageSrc = URL.createObjectURL(file);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
state.documents[type] = {
|
||||
file,
|
||||
uploadedFileName: null,
|
||||
imageSrc,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
export const setUploadedDocument = (type: UserDocumentTypes, fileName: string, url: string) => useUserStore.setState(
|
||||
produce<UserStore>(state => {
|
||||
state.documents[type] = {
|
||||
file: null,
|
||||
uploadedFileName: fileName,
|
||||
imageSrc: url,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
export const sendDocuments = () => {
|
||||
const state = useUserStore.getState();
|
||||
const type = state.dialogType;
|
||||
const documents = state.documents;
|
||||
|
||||
|
||||
|
||||
// const formData = new FormData();
|
||||
// formData.append("file1", file1);
|
||||
// formData.append("file2", file2);
|
||||
|
||||
// revoke on success
|
||||
// Object.values(documents).map(document => document?.imageSrc).forEach(src => {
|
||||
// if (src) URL.revokeObjectURL(src);
|
||||
// });
|
||||
|
||||
// useUserStore.setState(produce<UserStore>(state => {
|
||||
// state.isDocumentsDialogOpen = false;
|
||||
// }));
|
||||
};
|
||||
|
||||
export const setUser = (user: UserWithFields | null) => useUserStore.setState(
|
||||
produce<UserStore>(state => {
|
||||
state.user = user;
|
||||
if (!user) {
|
||||
state.settingsFields = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!state.settingsFields) state.settingsFields = { ...defaultFields };
|
||||
|
||||
state.settingsFields.name.value = user.name || "";
|
||||
state.settingsFields.surname.value = user.surname || "";
|
||||
state.settingsFields.middleName.value = user.middleName || "";
|
||||
state.settingsFields.companyName.value = user.companyName || "";
|
||||
state.settingsFields.email.value = user.email;
|
||||
state.settingsFields.phoneNumber.value = user.phoneNumber;
|
||||
state.settingsFields.password.value = "";
|
||||
})
|
||||
);
|
||||
|
||||
export const clearUser = () => useUserStore.setState({ ...initialState });
|
||||
|
||||
export const setSettingsField = (
|
||||
fieldName: UserSettingsField,
|
||||
value: string,
|
||||
) => useUserStore.setState(
|
||||
produce<UserStore>(state => {
|
||||
if (!state.settingsFields) return;
|
||||
|
||||
let errorMessage: string | null = null;
|
||||
|
||||
try {
|
||||
validators[fieldName].validateSync(value);
|
||||
} catch (error: any) {
|
||||
errorMessage = error.message;
|
||||
}
|
||||
|
||||
state.settingsFields[fieldName].value = value || "";
|
||||
state.settingsFields[fieldName].touched = true;
|
||||
state.settingsFields[fieldName].error = errorMessage;
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
export const sendUserData = async () => {
|
||||
const state = useUserStore.getState();
|
||||
if (!state.settingsFields) return;
|
||||
|
||||
const payload: PatchUserRequest = {};
|
||||
|
||||
for (const [fieldName, fieldValue] of Object.entries(state.settingsFields)) {
|
||||
if (
|
||||
fieldValue.value !== (state.user?.[fieldName as UserSettingsField] ?? "")
|
||||
) payload[fieldName as UserSettingsField] = fieldValue.value;
|
||||
}
|
||||
|
||||
const user = await patchUser(payload);
|
||||
setUser(user);
|
||||
};
|
||||
|
||||
const validators: Record<UserSettingsField, StringSchema> = {
|
||||
name: string(),
|
||||
email: string().email("Неверный email"),
|
||||
surname: string(),
|
||||
phoneNumber: string().matches(/^[+\d|\d]*$/, "Неверный номер телефона").min(6, "Номер телефона должен содержать минимум 6 символов"),
|
||||
middleName: string(),
|
||||
password: string().min(8, "Минимум 8 символов").matches(/^[.,:;-_+\d\w]+$/, "Некорректные символы в пароле"),
|
||||
companyName: string(),
|
||||
};
|
Loading…
Reference in New Issue
Block a user