From 8809fe30971177ef72e3d78a2955a3501eccf4fc Mon Sep 17 00:00:00 2001 From: nflnkr Date: Wed, 8 Nov 2023 15:51:40 +0300 Subject: [PATCH] add auth --- package.json | 1 + src/App.tsx | 68 +++++++++ src/api/auth.ts | 76 ++++++++++ src/index.tsx | 56 ++------ src/pages/auth/Signin.tsx | 225 ++++++++++++++++++++++++++++++ src/pages/auth/Signup.tsx | 229 +++++++++++++++++++++++++++++++ src/stores/user.ts | 48 +++++++ src/ui_kit/Header/HeaderFull.tsx | 22 ++- src/ui_kit/InputTextfield.tsx | 94 +++++++++++++ src/ui_kit/LogoutButton.tsx | 38 +++++ src/ui_kit/PenaLogo2.tsx | 35 +++++ src/ui_kit/passwordInput.tsx | 128 +++++++++++++++++ src/utils/parse-error.ts | 52 +++++++ yarn.lock | 47 +++++++ 14 files changed, 1071 insertions(+), 48 deletions(-) create mode 100644 src/App.tsx create mode 100644 src/api/auth.ts create mode 100644 src/pages/auth/Signin.tsx create mode 100644 src/pages/auth/Signup.tsx create mode 100644 src/stores/user.ts create mode 100644 src/ui_kit/InputTextfield.tsx create mode 100644 src/ui_kit/LogoutButton.tsx create mode 100644 src/ui_kit/PenaLogo2.tsx create mode 100644 src/ui_kit/passwordInput.tsx create mode 100644 src/utils/parse-error.ts diff --git a/package.json b/package.json index e2f543ff..1108e2ac 100755 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "dayjs": "^1.11.10", "emoji-mart": "^5.5.2", "file-saver": "^2.0.5", + "formik": "^2.4.5", "html-to-image": "^1.11.11", "immer": "^10.0.3", "jszip": "^3.10.1", diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 00000000..88333692 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,68 @@ +import ContactFormModal from "@ui_kit/ContactForm"; +import ImageCrop from "@ui_kit/Modal/ImageCrop"; +import dayjs from "dayjs"; +import "dayjs/locale/ru"; +import SigninDialog from "./pages/auth/Signin"; +import SignupDialog from "./pages/auth/Signup"; +import { BrowserRouter, Route, Routes } from "react-router-dom"; +import "./index.css"; +import ContactFormPage from "./pages/ContactFormPage/ContactFormPage"; +import InstallQuiz from "./pages/InstallQuiz/InstallQuiz"; +import Landing from "./pages/Landing/Landing"; +import QuestionsPage from "./pages/Questions/QuestionsPage"; +import { Result } from "./pages/Result/Result"; +import { Setting } from "./pages/Result/Setting"; +import MyQuizzesFull from "./pages/createQuize/MyQuizzesFull"; +import Main from "./pages/main"; +import StartPage from "./pages/startPage/StartPage"; +import { clearAuthToken, getMessageFromFetchError, useUserFetcher } from "@frontend/kitui"; +import { clearUserData, setUser, useUserStore } from "@root/user"; +import { enqueueSnackbar } from "notistack"; + + +dayjs.locale("ru"); + +const routeslink = [ + { path: "/list", page: , header: false, sidebar: false }, + { path: "/questions/:quizId", page: , header: true, sidebar: true, }, + { path: "/contacts", page: , header: true, sidebar: true }, + { path: "/result", page: , header: true, sidebar: true }, + { path: "/settings", page: , header: true, sidebar: true }, + { path: "/install", page: , header: true, sidebar: true }, +] as const; + +export default function App() { + const userId = useUserStore((state) => state.userId); + + useUserFetcher({ + url: `https://hub.pena.digital/user/${userId}`, + userId, + onNewUser: setUser, + onError: (error) => { + const errorMessage = getMessageFromFetchError(error); + if (errorMessage) { + enqueueSnackbar(errorMessage); + clearUserData(); + clearAuthToken(); + } + }, + }); + + return ( + <> + + + + {routeslink.map((e, i) => ( + } /> + ))} + } /> + } /> + } /> + } /> + } /> + + + + ); +} diff --git a/src/api/auth.ts b/src/api/auth.ts new file mode 100644 index 00000000..088363f4 --- /dev/null +++ b/src/api/auth.ts @@ -0,0 +1,76 @@ +import { makeRequest } from "@frontend/kitui"; + + +import type { + LoginRequest, + LoginResponse, + RegisterRequest, + RegisterResponse, +} from "@frontend/kitui"; +import { parseAxiosError } from "../utils/parse-error"; + +const apiUrl = + process.env.NODE_ENV === "production" + ? "/auth" + : "https://squiz.pena.digital/auth"; + +export async function register( + login: string, + password: string, + phoneNumber: string +): Promise<[RegisterResponse | null, string?]> { + try { + const registerResponse = await makeRequest< + RegisterRequest, + RegisterResponse + >({ + url: apiUrl + "/register", + body: { login, password, phoneNumber }, + useToken: false, + withCredentials: true, + }); + + return [registerResponse]; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); + + return [null, `Не удалось зарегестрировать аккаунт. ${error}`]; + } +} + +export async function login( + login: string, + password: string +): Promise<[LoginResponse | null, string?]> { + try { + const loginResponse = await makeRequest({ + url: apiUrl + "/login", + body: { login, password }, + useToken: false, + withCredentials: true, + }); + + return [loginResponse]; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); + + return [null, `Не удалось войти. ${error}`]; + } +} + +export async function logout(): Promise<[unknown, string?]> { + try { + const logoutResponse = await makeRequest({ + url: apiUrl + "/logout", + method: "POST", + useToken: true, + withCredentials: true, + }); + + return [logoutResponse]; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); + + return [null, `Не удалось выйти. ${error}`]; + } +} diff --git a/src/index.tsx b/src/index.tsx index 21e6785b..d51bdc3b 100755 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,48 +1,22 @@ -import React from "react"; -import { createRoot } from "react-dom/client"; -import { DndProvider } from "react-dnd"; -import { HTML5Backend } from "react-dnd-html5-backend"; -import "./index.css"; -import { BrowserRouter, Route, Routes } from "react-router-dom"; -import lightTheme from "./utils/themes/light"; import { CssBaseline, ThemeProvider } from "@mui/material"; -import StartPage from "./pages/startPage/StartPage"; -import Main from "./pages/main"; -import QuestionsPage from "./pages/Questions/QuestionsPage"; -import ContactFormPage from "./pages/ContactFormPage/ContactFormPage"; -import InstallQuiz from "./pages/InstallQuiz/InstallQuiz"; -import { Result } from "./pages/Result/Result"; -import { Setting } from "./pages/Result/Setting"; -import MyQuizzesFull from "./pages/createQuize/MyQuizzesFull"; -import ContactFormModal from "@ui_kit/ContactForm"; -import ImageCrop from "@ui_kit/Modal/ImageCrop"; -import Landing from "./pages/Landing/Landing"; -import { SnackbarProvider } from 'notistack' import { LocalizationProvider } from "@mui/x-date-pickers"; import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; -import "dayjs/locale/ru"; -import dayjs from "dayjs"; import { ruRU } from '@mui/x-date-pickers/locales'; +import App from "./App"; +import dayjs from "dayjs"; +import "dayjs/locale/ru"; +import { SnackbarProvider } from 'notistack'; +import { DndProvider } from "react-dnd"; +import { HTML5Backend } from "react-dnd-html5-backend"; +import { createRoot } from "react-dom/client"; +import "./index.css"; +import lightTheme from "./utils/themes/light"; dayjs.locale("ru"); const localeText = ruRU.components.MuiLocalizationProvider.defaultProps.localeText; -const routeslink: { - path: string; - page: JSX.Element; - header: boolean; - sidebar: boolean; -}[] = [ - { path: "/list", page: , header: false, sidebar: false }, - { path: "/questions/:quizId", page: , header: true, sidebar: true,}, - { path: "/contacts", page: , header: true, sidebar: true }, - { path: "/result", page: , header: true, sidebar: true }, - { path: "/settings", page: , header: true, sidebar: true }, - { path: "/install", page: , header: true, sidebar: true }, -]; - const root = createRoot(document.getElementById("root")!); root.render( @@ -51,17 +25,7 @@ root.render( - - - - {routeslink.map((e, i) => ( - } /> - ))} - } /> - } /> - }/> - - + diff --git a/src/pages/auth/Signin.tsx b/src/pages/auth/Signin.tsx new file mode 100644 index 00000000..1c6d8646 --- /dev/null +++ b/src/pages/auth/Signin.tsx @@ -0,0 +1,225 @@ +import { login } from "@api/auth"; +import CloseIcon from "@mui/icons-material/Close"; +import { + Box, + Button, + Dialog, + IconButton, + Link, + Typography, + useMediaQuery, + useTheme, +} from "@mui/material"; +import { setUserId, useUserStore } from "@root/user"; +import InputTextfield from "@ui_kit/InputTextfield"; +import PenaLogo2 from "@ui_kit/PenaLogo2"; +import PasswordInput from "@ui_kit/passwordInput"; +import { useFormik } from "formik"; +import { enqueueSnackbar } from "notistack"; +import { useEffect, useState } from "react"; +import { Link as RouterLink, useNavigate } from "react-router-dom"; +import { object, string } from "yup"; + +interface Values { + email: string; + password: string; +} + +const initialValues: Values = { + email: "", + password: "", +}; + +const validationSchema = object({ + email: string() + .required("Поле обязательно") + .email("Введите корректный email"), + password: string().required("Поле обязательно").min(8, "Минимум 8 символов"), +}); + +export default function SigninDialog() { + const [isDialogOpen, setIsDialogOpen] = useState(true); + const user = useUserStore((state) => state.user); + const theme = useTheme(); + const upMd = useMediaQuery(theme.breakpoints.up("md")); + const navigate = useNavigate(); + const formik = useFormik({ + initialValues, + validationSchema, + onSubmit: async (values, formikHelpers) => { + const [loginResponse, loginError] = await login( + values.email.trim(), + values.password.trim() + ); + + formikHelpers.setSubmitting(false); + + if (loginError) { + return enqueueSnackbar(loginError); + } + + if (loginResponse) { + setUserId(loginResponse._id); + } + }, + }); + + useEffect( + function redirectIfSignedIn() { + if (user) navigate("/list", { replace: true }); + }, + [navigate, user] + ); + + function handleClose() { + setIsDialogOpen(false); + setTimeout(() => navigate("/"), theme.transitions.duration.leavingScreen); + } + + return ( + + + + + + + + + + Вход в личный кабинет + + + + + {/* + Забыли пароль? + */} + + + Вы еще не присоединились? + + + Регистрация + + + + + ); +} diff --git a/src/pages/auth/Signup.tsx b/src/pages/auth/Signup.tsx new file mode 100644 index 00000000..d5d42698 --- /dev/null +++ b/src/pages/auth/Signup.tsx @@ -0,0 +1,229 @@ +import { register } from "@api/auth"; +import CloseIcon from "@mui/icons-material/Close"; +import { + Box, + Button, + Dialog, + IconButton, + Link, + Typography, + useMediaQuery, + useTheme, +} from "@mui/material"; +import { setUserId, useUserStore } from "@root/user"; +import InputTextfield from "@ui_kit/InputTextfield"; +import PenaLogo2 from "@ui_kit/PenaLogo2"; +import PasswordInput from "@ui_kit/passwordInput"; +import { useFormik } from "formik"; +import { enqueueSnackbar } from "notistack"; +import { useEffect, useState } from "react"; +import { Link as RouterLink, useNavigate } from "react-router-dom"; +import { object, ref, string } from "yup"; + +interface Values { + email: string; + password: string; + repeatPassword: string; +} + +const initialValues: Values = { + email: "", + password: "", + repeatPassword: "", +}; + +const validationSchema = object({ + email: string() + .required("Поле обязательно") + .email("Введите корректный email"), + password: string() + .min(8, "Минимум 8 символов") + .matches(/^[.,:;-_+\d\w]+$/, "Некорректные символы") + .required("Поле обязательно"), + repeatPassword: string() + .oneOf([ref("password"), undefined], "Пароли не совпадают") + .required("Повторите пароль"), +}); + +export default function SignupDialog() { + const [isDialogOpen, setIsDialogOpen] = useState(true); + const user = useUserStore((state) => state.user); + const theme = useTheme(); + const upMd = useMediaQuery(theme.breakpoints.up("md")); + const navigate = useNavigate(); + const formik = useFormik({ + initialValues, + validationSchema, + onSubmit: async (values, formikHelpers) => { + const [registerResponse, registerError] = await register( + values.email.trim(), + values.password.trim(), + "+7" + ); + + formikHelpers.setSubmitting(false); + + if (registerError) { + return enqueueSnackbar(registerError); + } + + if (registerResponse) { + setUserId(registerResponse._id); + } + }, + }); + + useEffect( + function redirectIfSignedIn() { + if (user) navigate("/list", { replace: true }); + }, + [navigate, user] + ); + + function handleClose() { + setIsDialogOpen(false); + setTimeout(() => navigate("/"), theme.transitions.duration.leavingScreen); + } + + return ( + + + + + + + + + + Регистрация + + + + + + + Вход в личный кабинет + + + + ); +} diff --git a/src/stores/user.ts b/src/stores/user.ts new file mode 100644 index 00000000..a6e5ad16 --- /dev/null +++ b/src/stores/user.ts @@ -0,0 +1,48 @@ +import { User } from "@frontend/kitui"; +import { produce } from "immer"; +import { create } from "zustand"; +import { createJSONStorage, devtools, persist } from "zustand/middleware"; + +interface UserStore { + userId: string | null; + user: User | null; + // userAccount: UserAccount | null; +} + +const initialState: UserStore = { + userId: null, + user: null, +}; + +export const useUserStore = create()( + persist( + devtools((set, get) => initialState, { + name: "User", + enabled: process.env.NODE_ENV === "development", + trace: true, + }), + { + version: 2, + name: "user", + storage: createJSONStorage(() => localStorage), + partialize: (state) => ({ + userId: state.userId, + user: state.user, + }), + migrate: (persistedState, version) => ({ + ...(persistedState as UserStore), + user: null, + }), + } + ) +); + +export const setUserId = (userId: string | null) => useUserStore.setState({ userId }); +export const setUser = (user: User) => + useUserStore.setState( + produce((state) => { + state.user = user; + }) + ); + +export const clearUserData = () => useUserStore.setState({ ...initialState }); diff --git a/src/ui_kit/Header/HeaderFull.tsx b/src/ui_kit/Header/HeaderFull.tsx index e472c42f..12b121c3 100644 --- a/src/ui_kit/Header/HeaderFull.tsx +++ b/src/ui_kit/Header/HeaderFull.tsx @@ -6,18 +6,36 @@ import { useTheme, useMediaQuery, } from "@mui/material"; -import LogoutIcon from "@icons/LogoutIcon"; import NavMenuItem from "./NavMenuItem"; import PenaLogo from "../PenaLogo"; import WalletIcon from "@icons/WalletIcon"; import CustomAvatar from "./Avatar"; import { Burger } from "@icons/Burger"; +import { clearAuthToken } from "@frontend/kitui"; +import { logout } from "@api/auth"; +import { useNavigate } from "react-router-dom"; +import { enqueueSnackbar } from "notistack"; +import { clearUserData } from "@root/user"; +import { LogoutButton } from "@ui_kit/LogoutButton"; export default function HeaderFull() { const theme = useTheme(); + const navigate = useNavigate(); const isTablet = useMediaQuery(theme.breakpoints.down(1000)); const isMobile = useMediaQuery(theme.breakpoints.down(500)); + async function handleLogoutClick() { + const [, logoutError] = await logout(); + + if (logoutError) { + return enqueueSnackbar(logoutError); + } + + clearAuthToken(); + clearUserData(); + navigate("/"); + } + return ( - + )} diff --git a/src/ui_kit/InputTextfield.tsx b/src/ui_kit/InputTextfield.tsx new file mode 100644 index 00000000..5a9e2856 --- /dev/null +++ b/src/ui_kit/InputTextfield.tsx @@ -0,0 +1,94 @@ +import { + FormControl, + InputLabel, + SxProps, + TextField, + TextFieldProps, + Theme, + useMediaQuery, + useTheme, +} from "@mui/material"; + +interface Props { + id: string; + label?: string; + bold?: boolean; + gap?: string; + color?: string; + FormInputSx?: SxProps; + TextfieldProps: TextFieldProps; + onChange: (e: React.ChangeEvent) => void; +} + +export default function InputTextfield({ + id, + label, + bold = false, + gap = "10px", + onChange, + TextfieldProps, + color, + FormInputSx, +}: Props) { + const theme = useTheme(); + const upMd = useMediaQuery(theme.breakpoints.up("md")); + + const labelFont = upMd + ? bold + ? theme.typography.p1 + : { ...theme.typography.body1, fontWeight: 500 } + : theme.typography.body2; + + const placeholderFont = upMd ? undefined : { fontWeight: 400, fontSize: "16px", lineHeight: "19px" }; + + return ( + + {label && ( + + {label} + + )} + + + ); +} diff --git a/src/ui_kit/LogoutButton.tsx b/src/ui_kit/LogoutButton.tsx new file mode 100644 index 00000000..b9e4ae10 --- /dev/null +++ b/src/ui_kit/LogoutButton.tsx @@ -0,0 +1,38 @@ +import { IconButton, IconButtonProps } from "@mui/material"; +import { deepmerge } from "@mui/utils"; + + +export function LogoutButton(props: IconButtonProps) { + + return ( + + + + + + ); +} diff --git a/src/ui_kit/PenaLogo2.tsx b/src/ui_kit/PenaLogo2.tsx new file mode 100644 index 00000000..7d840f7e --- /dev/null +++ b/src/ui_kit/PenaLogo2.tsx @@ -0,0 +1,35 @@ +import { useTheme } from "@mui/material"; + + +interface Props { + width: number; + color: string; +} + +export default function PenaLogo2({ width, color }: Props) { + const theme = useTheme(); + + return ( + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/src/ui_kit/passwordInput.tsx b/src/ui_kit/passwordInput.tsx new file mode 100644 index 00000000..bb31f0ec --- /dev/null +++ b/src/ui_kit/passwordInput.tsx @@ -0,0 +1,128 @@ +import { + FormControl, + IconButton, + InputLabel, + SxProps, + TextField, + TextFieldProps, + Theme, + useMediaQuery, + useTheme, +} from "@mui/material"; +import * as React from "react"; +import InputAdornment from "@mui/material/InputAdornment"; +import Visibility from "@mui/icons-material/Visibility"; +import VisibilityOff from "@mui/icons-material/VisibilityOff"; + +interface Props { + id: string; + label?: string; + bold?: boolean; + gap?: string; + color?: string; + FormInputSx?: SxProps; + TextfieldProps: TextFieldProps; + onChange: (e: React.ChangeEvent) => void; +} + +export default function PasswordInput({ + id, + label, + bold = false, + gap = "10px", + onChange, + TextfieldProps, + color, + FormInputSx, +}: Props) { + const theme = useTheme(); + const upMd = useMediaQuery(theme.breakpoints.up("md")); + + const labelFont = upMd + ? bold + ? theme.typography.p1 + : { ...theme.typography.body1, fontWeight: 500 } + : theme.typography.body2; + + const placeholderFont = upMd ? undefined : { fontWeight: 400, fontSize: "16px", lineHeight: "19px" }; + + const [showPassword, setShowPassword] = React.useState(false); + + const handleClickShowPassword = () => setShowPassword((show) => !show); + + const handleMouseDownPassword = (event: React.MouseEvent) => { + event.preventDefault(); + }; + + return ( + + + {label} + + + + {showPassword ? : } + + + ), + sx: { + padding: "0px", + border: "1px solid #9A9AAF", + backgroundColor: color, + borderRadius: "8px", + height: "48px", + color: "black", + ...placeholderFont, + "& .MuiInputBase-input": { + boxSizing: "border-box", + height: "100%", + padding: "14px", + }, + }, + }} + onChange={onChange} + type={showPassword ? "text" : "password"} + /> + + ); +} diff --git a/src/utils/parse-error.ts b/src/utils/parse-error.ts new file mode 100644 index 00000000..518756ac --- /dev/null +++ b/src/utils/parse-error.ts @@ -0,0 +1,52 @@ +import type { AxiosError } from "axios"; + +export type ServerError = { + statusCode: number; + error: string; + message: string; +}; + +const translateMessage: Record = { + "user not found": "Пользователь не найден", + "invalid password": "Неправильный пароль", + "field is empty": "Поле \"Пароль\" не заполнено", + "field is empty": "Поле \"Логин\" не заполнено", + "field is empty": "Поле \"E-mail\" не заполнено", + "field is empty": "Поле \"Номер телефона\" не заполнено", + "user with this email or login is exist": "Пользователь уже существует", + "user with this login is exist": "Пользователь с таким логином уже существует" +}; + +export const parseAxiosError = (nativeError: unknown): [string, number?] => { + const error = nativeError as AxiosError; + + if ( + error.response?.data && + "statusCode" in (error.response.data as ServerError) + ) { + const serverError = error.response.data as ServerError; + const translatedMessage = translateMessage[serverError.message] + if (translatedMessage !== undefined) serverError.message = translatedMessage + return [serverError.message, serverError.statusCode]; + } + + switch (error.status) { + case 404: + return ["Не найдено.", error.status]; + + case 403: + return ["Доступ ограничен.", error.status]; + + case 401: + return ["Ошибка авторизации.", error.status]; + + case 500: + return ["Внутренняя ошибка сервера.", error.status]; + + case 503: + return ["Сервис недоступен.", error.status]; + + default: + return ["Неизвестная ошибка сервера."]; + } +}; diff --git a/yarn.lock b/yarn.lock index 88e0142b..3cdffd0f 100755 --- a/yarn.lock +++ b/yarn.lock @@ -2263,6 +2263,14 @@ "@types/react" "*" hoist-non-react-statics "^3.3.0" +"@types/hoist-non-react-statics@^3.3.1": + version "3.3.5" + resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz#dab7867ef789d87e2b4b0003c9d65c49cc44a494" + integrity sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg== + dependencies: + "@types/react" "*" + hoist-non-react-statics "^3.3.0" + "@types/html-minifier-terser@^6.0.0": version "6.1.0" resolved "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz" @@ -3992,6 +4000,11 @@ deep-is@^0.1.3, deep-is@~0.1.3: resolved "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz" integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== +deepmerge@^2.1.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-2.2.1.tgz#5d3ff22a01c00f645405a2fbc17d0778a1801170" + integrity sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA== + deepmerge@^4.2.2: version "4.2.2" resolved "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz" @@ -4961,6 +4974,20 @@ form-data@^4.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" +formik@^2.4.5: + version "2.4.5" + resolved "https://registry.yarnpkg.com/formik/-/formik-2.4.5.tgz#f899b5b7a6f103a8fabb679823e8fafc7e0ee1b4" + integrity sha512-Gxlht0TD3vVdzMDHwkiNZqJ7Mvg77xQNfmBRrNtvzcHZs72TJppSTDKHpImCMJZwcWPBJ8jSQQ95GJzXFf1nAQ== + dependencies: + "@types/hoist-non-react-statics" "^3.3.1" + deepmerge "^2.1.1" + hoist-non-react-statics "^3.3.0" + lodash "^4.17.21" + lodash-es "^4.17.21" + react-fast-compare "^2.0.1" + tiny-warning "^1.0.2" + tslib "^2.0.0" + forwarded@0.2.0: version "0.2.0" resolved "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz" @@ -6527,6 +6554,11 @@ locate-path@^6.0.0: dependencies: p-locate "^5.0.0" +lodash-es@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee" + integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== + lodash.debounce@^4.0.8: version "4.0.8" resolved "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz" @@ -8011,6 +8043,11 @@ react-error-overlay@^6.0.11: resolved "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz" integrity sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg== +react-fast-compare@^2.0.1: + version "2.0.4" + resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9" + integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw== + react-image-crop@^10.1.5: version "10.1.8" resolved "https://registry.npmjs.org/react-image-crop/-/react-image-crop-10.1.8.tgz" @@ -9121,6 +9158,11 @@ tiny-invariant@^1.0.6: resolved "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz" integrity sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw== +tiny-warning@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" + integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== + tmpl@1.0.5: version "1.0.5" resolved "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz" @@ -9221,6 +9263,11 @@ tslib@^1.8.1: resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== +tslib@^2.0.0: + version "2.6.2" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" + integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== + tslib@^2.0.3: version "2.4.1" resolved "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz"