From 36fa9a63e9aeb2c41d97a816bd940d6f1d77670f Mon Sep 17 00:00:00 2001 From: nflnkr Date: Tue, 30 May 2023 21:34:41 +0300 Subject: [PATCH 1/3] WIP --- src/api/auth.ts | 18 +- src/api/user.ts | 28 ++ src/components/ComplexNavText.tsx | 72 ++--- src/index.tsx | 2 +- src/model/auth.ts | 13 - src/model/user.ts | 29 ++ src/pages/AccountSetup.tsx | 496 ++++++++++++++---------------- src/stores/makeRequest.ts | 2 + src/stores/user.ts | 100 +++++- 9 files changed, 434 insertions(+), 326 deletions(-) create mode 100644 src/api/user.ts create mode 100644 src/model/user.ts diff --git a/src/api/auth.ts b/src/api/auth.ts index 717cfa4..6219adb 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -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 { - return makeRequest({ - url: `${apiUrl}/${userId}`, - contentType: true, - method: "GET", - useToken: false, - withCredentials: false, - }); -} - export function logout() { return makeRequest({ - url: authUrl + "/logout", + url: apiUrl + "/auth/logout", method: "POST", - useToken: false, + useToken: true, withCredentials: true, }); } \ No newline at end of file diff --git a/src/api/user.ts b/src/api/user.ts new file mode 100644 index 0000000..1161d33 --- /dev/null +++ b/src/api/user.ts @@ -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 { + return makeRequest({ + url: `${apiUrl}/user/${userId}`, + contentType: true, + method: "GET", + useToken: false, + withCredentials: false, + }); +} + +export function patchUser(user: PatchUserRequest) { + return makeRequest({ + url: apiUrl + "/user/", + contentType: true, + method: "PATCH", + useToken: true, + withCredentials: false, + body: user, + }); +} \ No newline at end of file diff --git a/src/components/ComplexNavText.tsx b/src/components/ComplexNavText.tsx index 81432bb..82c04da 100644 --- a/src/components/ComplexNavText.tsx +++ b/src/components/ComplexNavText.tsx @@ -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 ( - - navigate("/tariffs")} - sx={{ - cursor: "pointer", - fontWeight: 400, - fontSize: "12px", - lineHeight: "14px", - color: theme.palette.grey2.main, - }} - > - {text1} - - - {text2} - - - ); + return ( + + navigate("/tariffs")} + sx={{ + cursor: "pointer", + fontWeight: 400, + fontSize: "12px", + lineHeight: "14px", + color: theme.palette.grey2.main, + }} + > + {text1} + + {text2 && + + {text2} + + } + + ); } diff --git a/src/index.tsx b/src/index.tsx index eada882..9b5a0c2 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -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"; diff --git a/src/model/auth.ts b/src/model/auth.ts index f0e6c76..f4ebcf2 100644 --- a/src/model/auth.ts +++ b/src/model/auth.ts @@ -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; -} \ No newline at end of file diff --git a/src/model/user.ts b/src/model/user.ts new file mode 100644 index 0000000..7191aeb --- /dev/null +++ b/src/model/user.ts @@ -0,0 +1,29 @@ +export interface User { + _id: string; + login: string; + email: string; + phoneNumber: string; + isDeleted: boolean; + createdAt: string; + updatedAt: string; + deletedAt?: string; +} + +export type UserWithFields = User & Partial>; + +export type UserSettingsField = + | "password" + | "email" + | "phoneNumber" + | "name" + | "surname" + | "middleName" + | "companyName"; + +export type UserSettings = Record; + +export type PatchUserRequest = Partial>; diff --git a/src/pages/AccountSetup.tsx b/src/pages/AccountSetup.tsx index a4a423e..3b8173c 100644 --- a/src/pages/AccountSetup.tsx +++ b/src/pages/AccountSetup.tsx @@ -1,272 +1,252 @@ -import { Box, Button, Link, Typography, useMediaQuery, useTheme } from "@mui/material"; +import { Box, Link, SxProps, Theme, 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"; +import ComplexNavText from "@root/components/ComplexNavText"; +import { sendUserData, setSettingsField, useUserStore } from "@root/stores/user"; + export default function AccountSetup() { - const theme = useTheme(); - const upMd = useMediaQuery(theme.breakpoints.up("md")); + const theme = useTheme(); + const upMd = useMediaQuery(theme.breakpoints.up("md")); + const upSm = useMediaQuery(theme.breakpoints.up("sm")); + const nameField = useUserStore(state => state.settingsFields?.name); + const emailField = useUserStore(state => state.settingsFields?.email); + const surnameField = useUserStore(state => state.settingsFields?.surname); + const phoneNumberField = useUserStore(state => state.settingsFields?.phoneNumber); + const middleNameField = useUserStore(state => state.settingsFields?.middleName); + const passwordField = useUserStore(state => state.settingsFields?.password); + const companyNameField = useUserStore(state => state.settingsFields?.companyName); + + const textFieldProps = { + gap: upMd ? "16px" : "10px", + color: "#F2F3F7", + bold: true, + }; - const [name, setName] = useState(""); - const [email, setEmail] = useState(""); - const [surname, setSurname] = useState(""); - const [telephone, setTelephone] = useState(""); - const [otchestvo, setOtchestvo] = useState(""); - const [password, setPassword] = useState(""); - const [сompanyName, setCompanyName] = useState(""); - - const [avatar, setAvatar] = useState(); - - const imgHC = (imgInp: any) => { - if (imgInp.target.files !== null) { - const file = imgInp.target.files[0]; - setAvatar(URL.createObjectURL(file)); - handleClose(); - } - }; - - return ( - - - Настройки аккаунта - - - - Настройки аккаунта - - - - - - - Account - - - - - - - setName(event.target.value)} - id="text" - label="Имя" - gap={upMd ? "15px" : "10px"} - color={name ? "#e8badd" : ""} - FormInputSx={{ width: upMd ? "45%" : "100%", mt: "19px", maxWidth: "406px" }} - /> - - 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" : ""} - /> - - setSurname(event.target.value)} - id="password" - label="Фамилия" - gap={upMd ? "15px" : "10px"} - FormInputSx={{ width: upMd ? "45%" : "100%", mt: "19px", maxWidth: "406px" }} - color={surname ? "#e8badd" : ""} - /> - - setTelephone(enent.target.value)} - id="password" - label="Телефон" - gap={upMd ? "15px" : "10px"} - FormInputSx={{ width: upMd ? "45%" : "100%", mt: "19px", maxWidth: "406px" }} - color={telephone ? "#e8badd" : ""} - /> - - setOtchestvo(enent.target.value)} - id="password" - label="Отчество" - gap={upMd ? "15px" : "10px"} - FormInputSx={{ width: upMd ? "45%" : "100%", mt: "19px", maxWidth: "406px" }} - color={otchestvo ? "#e8badd" : ""} - /> - - setPassword(enent.target.value)} - id="email" - label="Пароль" - gap={upMd ? "15px" : "10px"} - FormInputSx={{ width: upMd ? "45%" : "100%", mt: "19px", maxWidth: "406px" }} - color={password ? "#e8badd" : ""} - /> - - setCompanyName(enent.target.value)} - id="text" - label="Название компании" - gap={upMd ? "15px" : "10px"} - FormInputSx={{ width: upMd ? "45%" : "100%", mt: "19px", maxWidth: "406px" }} - color={сompanyName ? " #e8badd" : ""} - /> - - + + Настройки аккаунта + - - Download - + + + setSettingsField("name", e.target.value)} + id="name" + label="Имя" + {...textFieldProps} + /> + setSettingsField("surname", e.target.value)} + id="surname" + label="Фамилия" + {...textFieldProps} + /> + setSettingsField("middleName", e.target.value)} + id="middleName" + label="Отчество" + {...textFieldProps} + /> + setSettingsField("companyName", e.target.value)} + id="companyName" + label="Название компании" + {...textFieldProps} + /> + setSettingsField("email", e.target.value)} + id="email" + label="E-mail" + {...textFieldProps} + /> + setSettingsField("phoneNumber", e.target.value)} + id="phoneNumber" + label="Телефон" + {...textFieldProps} + /> + setSettingsField("password", e.target.value)} + id="password" + label="Пароль" + {...textFieldProps} + /> + + + Статус + + + Download + + Загрузить документы для юр лиц + + + + Download + + Загрузить документы для НКО + + + + + - Загрузить документы для юр лиц - - - - - Download - - Загрузить документы для НКО - - + Cохранить + - - - Cохранить - - - + + ); +} + +type VerificationStatus = "verificated" | "notVerificated" | "waiting"; + +const verificationStatusData: Record = { + "verificated": { + text: "Верификация пройдена", + color: "#0D9F00", + }, + "waiting": { + text: "В ожидании верификации", + color: "#F18956", + }, + "notVerificated": { + text: "Не верифицирован", + color: "#E02C2C", + }, +}; + +function VerificationIndicator({ verificationStatus, sx }: { + verificationStatus: VerificationStatus; + sx?: SxProps; +}) { + return ( + + {verificationStatusData[verificationStatus].text} - - - ); -} -function handleClose() { - throw new Error("Function not implemented."); -} + ); +} \ No newline at end of file diff --git a/src/stores/makeRequest.ts b/src/stores/makeRequest.ts index 7ab6ce0..488da6e 100644 --- a/src/stores/makeRequest.ts +++ b/src/stores/makeRequest.ts @@ -15,9 +15,11 @@ interface FirstRequest { method?: string; url: string; body?: T; + /** Send access token */ useToken?: boolean; contentType?: boolean; signal?: AbortSignal; + /** Send refresh token */ withCredentials?: boolean; } diff --git a/src/stores/user.ts b/src/stores/user.ts index e7c74c8..f3efd0e 100644 --- a/src/stores/user.ts +++ b/src/stores/user.ts @@ -1,16 +1,37 @@ -import { User } from "@root/model/auth"; +import { PatchUserRequest, UserSettings, UserSettingsField, UserWithFields } 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; } +const defaultFieldValues = { + value: "", + error: null, + touched: false, +}; + +const defaultFields = { + name: { ...defaultFieldValues }, + surname: { ...defaultFieldValues }, + middleName: { ...defaultFieldValues }, + companyName: { ...defaultFieldValues }, + email: { ...defaultFieldValues }, + phoneNumber: { ...defaultFieldValues }, + password: { ...defaultFieldValues }, +}; + const initialState: UserStore = { userId: null, user: null, + settingsFields: { ...defaultFields }, }; export const useUserStore = create()( @@ -19,16 +40,87 @@ export const useUserStore = create()( (set, get) => initialState, { name: "User store", + enabled: process.env.NODE_ENV === "development", } ), { 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 setUser = (user: UserWithFields | null) => useUserStore.setState( + produce(state => { + state.user = user; + if (!user) { + state.settingsFields = null; + return; + } -export const clearUser = () => useUserStore.setState({ ...initialState }); \ No newline at end of file + 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 = async ( + fieldName: UserSettingsField, + value: string, +) => { + const state = useUserStore.getState(); + let errorMessage: string | null = null; + + try { + validators[fieldName].validateSync(value); + } catch (error: any) { + errorMessage = error.message; + } + + useUserStore.setState(produce(state, state => { + if (!state.settingsFields) return; + + 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 = { + 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(), +}; \ No newline at end of file From e0412b2754072571d4f529e3f238a1a20d8fbd72 Mon Sep 17 00:00:00 2001 From: nflnkr Date: Thu, 1 Jun 2023 18:17:34 +0300 Subject: [PATCH 2/3] minor fixes --- src/pages/auth/Signin.tsx | 5 +++-- src/pages/auth/Signup.tsx | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/pages/auth/Signin.tsx b/src/pages/auth/Signin.tsx index 8b640cd..46fa1c5 100644 --- a/src/pages/auth/Signin.tsx +++ b/src/pages/auth/Signin.tsx @@ -45,8 +45,8 @@ export default function SigninDialog() { makeRequest({ 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, @@ -78,6 +78,7 @@ export default function SigninDialog() { PaperProps={{ sx: { width: "600px", + maxWidth: "600px", } }} slotProps={{ diff --git a/src/pages/auth/Signup.tsx b/src/pages/auth/Signup.tsx index cf37db2..6626bf0 100644 --- a/src/pages/auth/Signup.tsx +++ b/src/pages/auth/Signup.tsx @@ -48,8 +48,8 @@ export default function SignupDialog() { makeRequest({ 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={{ From ad02a6a4b863df1a44a223882d3f8bb3cecdb78a Mon Sep 17 00:00:00 2001 From: nflnkr Date: Fri, 2 Jun 2023 11:22:14 +0300 Subject: [PATCH 3/3] add account setup document dialogs --- src/components/UnderlinedButtonWithIcon.tsx | 45 ++++++ src/components/icons/CloseSmallIcon.tsx | 21 +++ src/components/icons/EyeIcon.tsx | 21 +++ src/components/icons/PaperClipIcon.tsx | 24 ++++ src/components/icons/UploadIcon.tsx | 22 +++ src/index.tsx | 2 +- src/model/user.ts | 15 ++ src/pages/{ => AccountSetup}/AccountSetup.tsx | 128 +++++++++--------- .../DocumentsDialog/DocumentItem.tsx | 45 ++++++ .../DocumentsDialog/DocumentUploadItem.tsx | 62 +++++++++ .../DocumentsDialog/DocumentsDialog.tsx | 10 ++ .../JuridicalDocumentsDialog.tsx | 107 +++++++++++++++ .../DocumentsDialog/NkoDocumentsDialog.tsx | 116 ++++++++++++++++ src/stores/user.ts | 122 +++++++++++++++-- 14 files changed, 657 insertions(+), 83 deletions(-) create mode 100644 src/components/UnderlinedButtonWithIcon.tsx create mode 100644 src/components/icons/CloseSmallIcon.tsx create mode 100644 src/components/icons/EyeIcon.tsx create mode 100644 src/components/icons/PaperClipIcon.tsx create mode 100644 src/components/icons/UploadIcon.tsx rename src/pages/{ => AccountSetup}/AccountSetup.tsx (63%) create mode 100644 src/pages/AccountSetup/DocumentsDialog/DocumentItem.tsx create mode 100644 src/pages/AccountSetup/DocumentsDialog/DocumentUploadItem.tsx create mode 100644 src/pages/AccountSetup/DocumentsDialog/DocumentsDialog.tsx create mode 100644 src/pages/AccountSetup/DocumentsDialog/JuridicalDocumentsDialog.tsx create mode 100644 src/pages/AccountSetup/DocumentsDialog/NkoDocumentsDialog.tsx diff --git a/src/components/UnderlinedButtonWithIcon.tsx b/src/components/UnderlinedButtonWithIcon.tsx new file mode 100644 index 0000000..591fbaf --- /dev/null +++ b/src/components/UnderlinedButtonWithIcon.tsx @@ -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; + onClick?: MouseEventHandler; +} + +export default function UnderlinedButtonWithIcon({ ButtonProps, icon, children, sx, onClick }: Props) { + const theme = useTheme(); + const upMd = useMediaQuery(theme.breakpoints.up("md")); + + return ( + + ); +} \ No newline at end of file diff --git a/src/components/icons/CloseSmallIcon.tsx b/src/components/icons/CloseSmallIcon.tsx new file mode 100644 index 0000000..a043fba --- /dev/null +++ b/src/components/icons/CloseSmallIcon.tsx @@ -0,0 +1,21 @@ +import { Box } from "@mui/material"; + + +export default function CloseSmallIcon() { + + return ( + + + + + + + ); +} \ No newline at end of file diff --git a/src/components/icons/EyeIcon.tsx b/src/components/icons/EyeIcon.tsx new file mode 100644 index 0000000..6785638 --- /dev/null +++ b/src/components/icons/EyeIcon.tsx @@ -0,0 +1,21 @@ +import { Box } from "@mui/material"; + + +export default function EyeIcon() { + + return ( + + + + + + + ); +} \ No newline at end of file diff --git a/src/components/icons/PaperClipIcon.tsx b/src/components/icons/PaperClipIcon.tsx new file mode 100644 index 0000000..178151d --- /dev/null +++ b/src/components/icons/PaperClipIcon.tsx @@ -0,0 +1,24 @@ +import { Box } from "@mui/material"; + + +interface Props { + color?: string; +} + +export default function PaperClipIcon({ color = "#7E2AEA" }: Props) { + + return ( + + + + + + ); +} \ No newline at end of file diff --git a/src/components/icons/UploadIcon.tsx b/src/components/icons/UploadIcon.tsx new file mode 100644 index 0000000..6c0add8 --- /dev/null +++ b/src/components/icons/UploadIcon.tsx @@ -0,0 +1,22 @@ +import { Box } from "@mui/material"; + + +export default function UploadIcon() { + + return ( + + + + + + + + ); +} \ No newline at end of file diff --git a/src/index.tsx b/src/index.tsx index 9b5a0c2..93faccf 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -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"; diff --git a/src/model/user.ts b/src/model/user.ts index 7191aeb..77e35ce 100644 --- a/src/model/user.ts +++ b/src/model/user.ts @@ -27,3 +27,18 @@ export type UserSettings = Record; export type PatchUserRequest = Partial>; + +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; diff --git a/src/pages/AccountSetup.tsx b/src/pages/AccountSetup/AccountSetup.tsx similarity index 63% rename from src/pages/AccountSetup.tsx rename to src/pages/AccountSetup/AccountSetup.tsx index 3b8173c..1f9e571 100644 --- a/src/pages/AccountSetup.tsx +++ b/src/pages/AccountSetup/AccountSetup.tsx @@ -1,24 +1,24 @@ -import { Box, Link, SxProps, Theme, Typography, useMediaQuery, useTheme } from "@mui/material"; +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 Download from "../assets/Icons/Download.svg"; import ComplexNavText from "@root/components/ComplexNavText"; -import { sendUserData, setSettingsField, useUserStore } from "@root/stores/user"; +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 nameField = useUserStore(state => state.settingsFields?.name); - const emailField = useUserStore(state => state.settingsFields?.email); - const surnameField = useUserStore(state => state.settingsFields?.surname); - const phoneNumberField = useUserStore(state => state.settingsFields?.phoneNumber); - const middleNameField = useUserStore(state => state.settingsFields?.middleName); - const passwordField = useUserStore(state => state.settingsFields?.password); - const companyNameField = useUserStore(state => state.settingsFields?.companyName); - + 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", @@ -33,6 +33,7 @@ export default function AccountSetup() { mb: "70px", }} > + Настройки аккаунта setSettingsField("name", e.target.value)} id="name" @@ -82,9 +83,9 @@ export default function AccountSetup() { setSettingsField("surname", e.target.value)} id="surname" @@ -94,9 +95,9 @@ export default function AccountSetup() { setSettingsField("middleName", e.target.value)} id="middleName" @@ -106,9 +107,9 @@ export default function AccountSetup() { setSettingsField("companyName", e.target.value)} id="companyName" @@ -118,9 +119,9 @@ export default function AccountSetup() { setSettingsField("email", e.target.value)} id="email" @@ -130,9 +131,9 @@ export default function AccountSetup() { setSettingsField("phoneNumber", e.target.value)} id="phoneNumber" @@ -142,9 +143,9 @@ export default function AccountSetup() { setSettingsField("password", e.target.value)} @@ -158,41 +159,36 @@ export default function AccountSetup() { }}> Статус - - Download - + } + sx={{ mt: "55px" }} + ButtonProps={{ + onClick: () => openDocumentsDialog("juridical"), + }} + >Загрузить документы для юр лиц + } + sx={{ mt: "15px" }} + ButtonProps={{ + onClick: () => openDocumentsDialog("nko"), + }} + >Загрузить документы для НКО + + } + {verificationStatus === "verificated" && + } + sx={{ mt: "55px" }} + ButtonProps={{ + onClick: () => openDocumentsDialog(verificationType), }} - > - Загрузить документы для юр лиц - - - - Download - - Загрузить документы для НКО - - + >Посмотреть свою верификацию + } - Cохранить + Сохранить ); } -type VerificationStatus = "verificated" | "notVerificated" | "waiting"; - const verificationStatusData: Record = { "verificated": { text: "Верификация пройдена", diff --git a/src/pages/AccountSetup/DocumentsDialog/DocumentItem.tsx b/src/pages/AccountSetup/DocumentsDialog/DocumentItem.tsx new file mode 100644 index 0000000..080b065 --- /dev/null +++ b/src/pages/AccountSetup/DocumentsDialog/DocumentItem.tsx @@ -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; +} + +export default function DocumentItem({ text, document, sx }: Props) { + const theme = useTheme(); + + return ( + + {text} + {document.uploadedFileName && + {document.uploadedFileName} + } + {document.imageSrc && + document} + + ); +} \ No newline at end of file diff --git a/src/pages/AccountSetup/DocumentsDialog/DocumentUploadItem.tsx b/src/pages/AccountSetup/DocumentsDialog/DocumentUploadItem.tsx new file mode 100644 index 0000000..328a0c7 --- /dev/null +++ b/src/pages/AccountSetup/DocumentsDialog/DocumentUploadItem.tsx @@ -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) => void; + sx?: SxProps; +} + +export default function DocumentUploadItem({ text, document, onFileChange, sx }: Props) { + const fileInputRef = useRef(null); + + function handleChooseFileClick() { + fileInputRef.current?.click(); + } + + return ( + + {text} + } + onClick={handleChooseFileClick} + >{document.file ? document.file.name : "Выберите файл"} + + {document.imageSrc && + document + } + + ); +} \ No newline at end of file diff --git a/src/pages/AccountSetup/DocumentsDialog/DocumentsDialog.tsx b/src/pages/AccountSetup/DocumentsDialog/DocumentsDialog.tsx new file mode 100644 index 0000000..8ad7209 --- /dev/null +++ b/src/pages/AccountSetup/DocumentsDialog/DocumentsDialog.tsx @@ -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" ? : +} \ No newline at end of file diff --git a/src/pages/AccountSetup/DocumentsDialog/JuridicalDocumentsDialog.tsx b/src/pages/AccountSetup/DocumentsDialog/JuridicalDocumentsDialog.tsx new file mode 100644 index 0000000..dea39e1 --- /dev/null +++ b/src/pages/AccountSetup/DocumentsDialog/JuridicalDocumentsDialog.tsx @@ -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" ? ( + <> + + + + ) : ( + <> + setDocument("ИНН", e.target?.files?.[0])} + /> + setDocument("Устав", e.target?.files?.[0])} + /> + + ); + + return ( + + + + + + + {verificationStatus === "verificated" ? "Ваши документы" : "Загрузите документы"} + + для верификации юридических лиц в формате PDF + + {documentElements} + + + + Отправить + + + ); +} \ No newline at end of file diff --git a/src/pages/AccountSetup/DocumentsDialog/NkoDocumentsDialog.tsx b/src/pages/AccountSetup/DocumentsDialog/NkoDocumentsDialog.tsx new file mode 100644 index 0000000..177682c --- /dev/null +++ b/src/pages/AccountSetup/DocumentsDialog/NkoDocumentsDialog.tsx @@ -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" ? ( + <> + + + + + ) : ( + <> + setDocument("Свидетельство о регистрации НКО", e.target?.files?.[0])} + /> + setDocument("ИНН", e.target?.files?.[0])} + /> + setDocument("Устав", e.target?.files?.[0])} + /> + + ); + + return ( + + + + + + + {verificationStatus === "verificated" ? "Ваши документы" : "Загрузите документы"} + + для верификации НКО в формате PDF + + {documentElements} + + + + Отправить + + + ); +} \ No newline at end of file diff --git a/src/stores/user.ts b/src/stores/user.ts index f3efd0e..fa437b5 100644 --- a/src/stores/user.ts +++ b/src/stores/user.ts @@ -1,4 +1,4 @@ -import { PatchUserRequest, UserSettings, UserSettingsField, UserWithFields } from "@root/model/user"; +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"; @@ -10,6 +10,11 @@ interface UserStore { userId: string | null; user: UserWithFields | null; settingsFields: UserSettings | null; + verificationStatus: VerificationStatus; + verificationType: "juridical" | "nko"; + isDocumentsDialogOpen: boolean; + dialogType: "juridical" | "nko"; + documents: UserDocuments; } const defaultFieldValues = { @@ -28,10 +33,25 @@ const defaultFields = { 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()( @@ -44,6 +64,7 @@ export const useUserStore = create()( } ), { + version: 1, name: "user", storage: createJSONStorage(() => localStorage), partialize: state => ({ @@ -55,6 +76,77 @@ export const useUserStore = create()( ); export const setUserId = (userId: string | null) => useUserStore.setState({ userId }); + +export const openDocumentsDialog = (type: UserStore["dialogType"]) => useUserStore.setState( + produce(state => { + state.isDocumentsDialogOpen = true; + state.dialogType = type; + }) +); + +export const closeDocumentsDialog = () => useUserStore.setState( + produce(state => { + state.isDocumentsDialogOpen = false; + }) +); + +export const setDocument = (type: UserDocumentTypes, file: File | undefined) => useUserStore.setState( + produce(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(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(state => { + // state.isDocumentsDialogOpen = false; + // })); +}; + export const setUser = (user: UserWithFields | null) => useUserStore.setState( produce(state => { state.user = user; @@ -77,27 +169,27 @@ export const setUser = (user: UserWithFields | null) => useUserStore.setState( export const clearUser = () => useUserStore.setState({ ...initialState }); -export const setSettingsField = async ( +export const setSettingsField = ( fieldName: UserSettingsField, value: string, -) => { - const state = useUserStore.getState(); - let errorMessage: string | null = null; - - try { - validators[fieldName].validateSync(value); - } catch (error: any) { - errorMessage = error.message; - } - - useUserStore.setState(produce(state, state => { +) => useUserStore.setState( + produce(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();