diff --git a/client/package.json b/client/package.json index 93fa6a9a..ed5d9722 100644 --- a/client/package.json +++ b/client/package.json @@ -35,13 +35,13 @@ "isomorphic-dompurify": "^2.2.0", "lowlight": "^3.1.0", "next": "^14.0.4", - "next-redux-wrapper": "^8.1.0", "openai": "^4.8.0", "postcss-nesting": "^12.0.1", "react": "^18.2.0", "react-dom": "^18.2.0", "react-error-boundary": "^4.0.11", "react-redux": "^8.0.5", + "redux-persist": "^6.0.0", "socketjs-client": "^1.0.2", "tiptap-extension-resize-image": "^1.0.2", "typescript": "5.0.2", diff --git a/client/public/images/check.png b/client/public/images/check.png new file mode 100644 index 00000000..a70c0a5b Binary files /dev/null and b/client/public/images/check.png differ diff --git a/client/src/app/StoreProvider.tsx b/client/src/app/StoreProvider.tsx index c858b8cd..a2e6ff19 100644 --- a/client/src/app/StoreProvider.tsx +++ b/client/src/app/StoreProvider.tsx @@ -2,7 +2,8 @@ import { useRef } from 'react'; import { Provider } from 'react-redux'; -import { makeStore, AppStore, store } from '../lib/redux/store'; +import { makeStore, AppStore, store, persistor } from '../lib/redux/store'; +import { PersistGate } from 'redux-persist/integration/react'; export default function StoreProvider({ children, @@ -15,5 +16,11 @@ export default function StoreProvider({ storeRef.current = makeStore(); } - return {children}; + return ( + + + {children} + + + ); } diff --git a/client/src/app/login/components/GithubLoginButton/index.tsx b/client/src/app/login/components/GithubLoginButton/index.tsx new file mode 100644 index 00000000..6b1b2633 --- /dev/null +++ b/client/src/app/login/components/GithubLoginButton/index.tsx @@ -0,0 +1,34 @@ +'use client'; + +import { Icon } from '@iconify/react'; + +//hooks +import useModal from '@/hooks/modal/useModal'; + +//constant +import { errorMessage, ModalType } from '@/constants/constant'; + +export default function GithubLoginButton() { + const { openModal } = useModal(); + + return ( +
+ +
+ ); +} diff --git a/client/src/app/login/components/GoogleLoginButton/index.tsx b/client/src/app/login/components/GoogleLoginButton/index.tsx new file mode 100644 index 00000000..568438d5 --- /dev/null +++ b/client/src/app/login/components/GoogleLoginButton/index.tsx @@ -0,0 +1,34 @@ +'use client'; + +import { Icon } from '@iconify/react'; + +//hooks +import useModal from '@/hooks/modal/useModal'; + +//constant +import { errorMessage, ModalType } from '@/constants/constant'; + +export default function GoogleLoginButton() { + const { openModal } = useModal(); + + return ( +
+ +
+ ); +} diff --git a/client/src/app/login/components/LoginForm/index.tsx b/client/src/app/login/components/LoginForm/index.tsx new file mode 100644 index 00000000..24ea0c5a --- /dev/null +++ b/client/src/app/login/components/LoginForm/index.tsx @@ -0,0 +1,188 @@ +'use client'; + +import { useState, useCallback } from 'react'; +import { useRouter } from 'next/navigation'; + +//redux +import { useDispatch, useSelector } from 'react-redux'; +import { logIn } from '@/lib/redux/features/auth/authSlice'; + +//components +import { Tooltip } from '@/components'; + +//hooks +import useModal from '@/hooks/modal/useModal'; + +//icons +import { EyeOnIcon, EyeOffIcon } from '@/assets/Icon'; + +//formik +import { Formik, Form, Field, ErrorMessage } from 'formik'; +import * as Yup from 'yup'; + +//services +import { userManager } from '@/service/user'; + +//constant +import { + initialLoginValue, + errorMessage, + errorCodeToMessage, + ModalType, +} from '@/constants/constant'; + +//types +import { IReduxState } from '@/types/redux/IReduxState'; + +const ValidationSchema = Yup.object().shape({ + userId: Yup.string().required(errorMessage.blankID), + password: Yup.string().required(errorMessage.blankPassword), +}); + +interface ILogin { + userId: string; + password: string; +} + +export default function LoginForm() { + const [viewPassword, setViewPassword] = useState(false); + + const dispatch = useDispatch(); + + const router = useRouter(); + + const modalState = useSelector((state: IReduxState) => state.modal); + const { openModal } = useModal(); + + const handleSubmit = useCallback( + async (sendData: ILogin, setSubmitting: (value: boolean) => void) => { + if (modalState.type === ModalType.CLOSE) { + setSubmitting(true); + try { + const userData = await userManager.loginUser({ data: sendData }); + dispatch(logIn(userData)); + router.replace(window.sessionStorage.getItem('previousURL') || '/'); + } catch (error) { + if (error instanceof Error) { + type Code = 'USER_NOT_EXIST' | 'INVALID_PASSWORD'; + openModal({ + type: ModalType.ERROR, + message: + errorCodeToMessage[error.message as Code] || errorMessage.error, + }); + } else { + openModal({ + type: ModalType.ERROR, + message: errorMessage.error, + }); + } + } finally { + setSubmitting(false); + } + } + }, + [modalState] + ); + return ( + handleSubmit(data, setSubmitting)} + > + {({ errors, touched, isSubmitting }) => ( +
+
+
+ + + +
+
+ +
+ +
setViewPassword((prev) => !prev)} + > + {viewPassword ? : } +
+
+ +
+
+
+ +
+ + +
+
+ + openModal({ + type: ModalType.ERROR, + message: errorMessage.notComplete, + }) + } + > + 비밀번호 찾기 + +
+
+ +
+
+ )} +
+ ); +} diff --git a/client/src/app/login/layout.tsx b/client/src/app/login/layout.tsx deleted file mode 100644 index ca088b9d..00000000 --- a/client/src/app/login/layout.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import React from 'react'; -import { Layout } from '@/components'; - -export default function LayoutComponent({ - children, -}: { - children: React.ReactNode; -}) { - return {children}; -} diff --git a/client/src/app/login/page.tsx b/client/src/app/login/page.tsx index fcc14c75..44d3199a 100644 --- a/client/src/app/login/page.tsx +++ b/client/src/app/login/page.tsx @@ -1,183 +1,37 @@ -'use client'; - -import { useCallback } from 'react'; import Link from 'next/link'; -import { useRouter } from 'next/navigation'; -import { useDispatch, useSelector } from 'react-redux'; -import { logIn } from '@/lib/redux/features/auth/authSlice'; - -// packages -import { Icon } from '@iconify/react'; -import { Formik, Form, Field, ErrorMessage } from 'formik'; -import * as Yup from 'yup'; // components -import { Divider } from '@/components'; - -// service -import { userManager } from '@/service/user'; - -// hooks -import useModal from '@/hooks/modal/useModal'; - -// constant -import { - initialLoginValue, - errorMessage, - errorCodeToMessage, - ModalType, -} from '@/constants/constant'; -import { IReduxState } from '@/types/redux/IReduxState'; - -const ValidationSchema = Yup.object().shape({ - userId: Yup.string().required(errorMessage.blankID), - password: Yup.string().required(errorMessage.blankPassword), -}); - -interface ILogin { - userId: string; - password: string; -} +import Logo from '@/components/Navigator/Logo'; +import LoginForm from './components/LoginForm'; +import GoogleLoginButton from './components/GoogleLoginButton'; +import GithubLoginButton from './components/GithubLoginButton'; export default function Login() { - const router = useRouter(); - const dispatch = useDispatch(); - - // Modal - const modalState = useSelector((state: IReduxState) => state.modal); - const { openModal } = useModal(); - - const handleSubmit = useCallback( - async (sendData: ILogin, setSubmitting: (value: boolean) => void) => { - if (modalState.type === ModalType.CLOSE) { - setSubmitting(true); - try { - const userData = await userManager.loginUser({ data: sendData }); - dispatch(logIn(userData)); - router.push('/'); - } catch (error) { - if (error instanceof Error) { - type Code = 'USER_NOT_EXIST' | 'INVALID_PASSWORD'; - openModal({ - type: ModalType.ERROR, - message: - errorCodeToMessage[error.message as Code] || errorMessage.error, - }); - } else { - openModal({ - type: ModalType.ERROR, - message: errorMessage.error, - }); - } - } finally { - setSubmitting(false); - } - } - }, - [modalState] - ); return ( -
-

로그인

- - handleSubmit(data, setSubmitting) - } - > - {({ errors, touched, isSubmitting }) => ( -
- - - - - -
- - -
- - 회원가입 - - - - openModal({ - type: ModalType.ERROR, - message: errorMessage.notComplete, - }) - } - > - 비밀번호찾기 - -
- - )} -
+
+
+
+ +
+

+ 로그인 +

+ +
+ + +
+ + {"아직 '리덕' 회원이 아니신가요?"} + + + 회원가입 하기 + +
+
); } diff --git a/client/src/assets/Icon/eye_off.tsx b/client/src/assets/Icon/eye_off.tsx new file mode 100644 index 00000000..15cdf691 --- /dev/null +++ b/client/src/assets/Icon/eye_off.tsx @@ -0,0 +1,45 @@ +import { SVGProps } from 'react'; + +export default function EyeOffIcon(props: SVGProps) { + return ( + + + + + + + ); +} diff --git a/client/src/assets/Icon/eye_on.tsx b/client/src/assets/Icon/eye_on.tsx new file mode 100644 index 00000000..5bb59370 --- /dev/null +++ b/client/src/assets/Icon/eye_on.tsx @@ -0,0 +1,29 @@ +import { SVGProps } from 'react'; + +export default function EyeOnIcon(props: SVGProps) { + return ( + + + + + ); +} diff --git a/client/src/assets/Icon/index.ts b/client/src/assets/Icon/index.ts index 7425cd5d..01084dec 100644 --- a/client/src/assets/Icon/index.ts +++ b/client/src/assets/Icon/index.ts @@ -17,3 +17,5 @@ export { default as LinkIcon } from './link'; export { default as ImageIcon } from './image'; export { default as ListIcon } from './list'; export { default as NumberListIcon } from './number-list'; +export { default as EyeOnIcon } from './eye_on'; +export { default as EyeOffIcon } from './eye_off'; diff --git a/client/src/components/Dropdown/Content/index.tsx b/client/src/components/Dropdown/Content/index.tsx new file mode 100644 index 00000000..20498272 --- /dev/null +++ b/client/src/components/Dropdown/Content/index.tsx @@ -0,0 +1,14 @@ +import { HTMLAttributes } from 'react'; + +const DropdownContent = (props: HTMLAttributes) => { + return ( +
+ {props.children} +
+ ); +}; + +export default DropdownContent; diff --git a/client/src/components/Dropdown/Wrapper/index.tsx b/client/src/components/Dropdown/Wrapper/index.tsx new file mode 100644 index 00000000..4919a15f --- /dev/null +++ b/client/src/components/Dropdown/Wrapper/index.tsx @@ -0,0 +1,35 @@ +'use client'; + +import { PropsWithChildren } from 'react'; + +// hooks +import useClickAwayRef from '@/hooks/common/useClickAwayRef'; + +interface IProps extends PropsWithChildren { + setDropdownOpen: (value: boolean) => void; + position?: string; +} + +const DropdownWrapper = ({ setDropdownOpen, children, position }: IProps) => { + const ref = useClickAwayRef((e) => { + if (!ref.current || !ref.current.parentNode) { + return; + } + if (!ref.current.parentNode.contains(e.target as HTMLElement)) { + setDropdownOpen(false); + } + }); + + const DROPDOWN_POSITION = position || 'right-0 -bottom-24'; + + return ( +
+ {children} +
+ ); +}; + +export default DropdownWrapper; diff --git a/client/src/components/Dropdown/index.tsx b/client/src/components/Dropdown/index.tsx new file mode 100644 index 00000000..3fa017ab --- /dev/null +++ b/client/src/components/Dropdown/index.tsx @@ -0,0 +1,9 @@ +import DropdownWrapper from './Wrapper'; +import DropdownContent from './Content'; + +const Dropdown = { + Wrapper: DropdownWrapper, + Content: DropdownContent, +}; + +export default Dropdown; diff --git a/client/src/components/Navigator/Logo.tsx b/client/src/components/Navigator/Logo.tsx new file mode 100644 index 00000000..9d6ccde4 --- /dev/null +++ b/client/src/components/Navigator/Logo.tsx @@ -0,0 +1,18 @@ +import Link from 'next/link'; +import Image from 'next/image'; + +export default function Logo() { + return ( + + reDuck-icon + + reDuck + + + ); +} diff --git a/client/src/components/Navigator/NavigatorBottom.tsx b/client/src/components/Navigator/NavigatorBottom.tsx new file mode 100644 index 00000000..6caf3ed5 --- /dev/null +++ b/client/src/components/Navigator/NavigatorBottom.tsx @@ -0,0 +1,40 @@ +'use client'; + +//components +import Button from '../base/Button'; +import PostModeNavigation from './PostModeNavigation'; + +//hooks +import useClientLoginCheck from '@/hooks/common/useClientLoginCheck'; + +//icons +import { ArrowDownIcon } from '@/assets/Icon'; + +//types +import { PostViewType } from '@/types'; + +interface IProps { + viewMode?: PostViewType; + initialUserLogin: boolean; +} + +export default function NavigatorBottom({ + viewMode, + initialUserLogin, +}: IProps) { + const userLogin = useClientLoginCheck(initialUserLogin); + + return ( +
+
+ + {userLogin && ( + + )} +
+
+ ); +} diff --git a/client/src/components/Navigator/NavigatorTop.tsx b/client/src/components/Navigator/NavigatorTop.tsx new file mode 100644 index 00000000..bd7dc5bb --- /dev/null +++ b/client/src/components/Navigator/NavigatorTop.tsx @@ -0,0 +1,65 @@ +'use client'; + +import Link from 'next/link'; +import { useRouter } from 'next/navigation'; + +//components +import Logo from './Logo'; +import SearchButton from './SearchButton'; +import AlarmButton from './AlarmButton'; +import ChatButton from './ChatButton'; +import ProfileButton from './ProfileButton'; +import Button from '../base/Button'; + +//hooks +import useClientLoginCheck from '@/hooks/common/useClientLoginCheck'; + +interface IProps { + initialUserLogin: boolean; +} + +export default function NavigatorTop({ initialUserLogin }: IProps) { + const userLogin = useClientLoginCheck(initialUserLogin); + + const router = useRouter(); + + const handleLinkToLogin = () => { + window.sessionStorage.setItem('previousURL', window.location.href); + router.push('login'); + }; + + return ( +
+ +
+
    +
  • + +
  • + {userLogin && ( + <> +
  • + +
  • +
  • + +
  • + + )} +
+ {userLogin ? ( + + ) : ( +
+ + + + +
+ )} +
+
+ ); +} diff --git a/client/src/components/Navigator/ProfileButton.tsx b/client/src/components/Navigator/ProfileButton.tsx index d0a4298f..eccbf0a9 100644 --- a/client/src/components/Navigator/ProfileButton.tsx +++ b/client/src/components/Navigator/ProfileButton.tsx @@ -1,9 +1,81 @@ +'use client'; + +import { useState } from 'react'; +import Link from 'next/link'; +import { useRouter } from 'next/navigation'; + +// redux +import { useSelector, useDispatch } from 'react-redux'; +import { selectUser } from '@/lib/redux/features/auth/authSlice'; +import { logOut } from '@/lib/redux/features/auth/authSlice'; + +// components +import { Avatar, Dropdown } from '../'; + +// hooks +import useModal from '@/hooks/modal/useModal'; + +// icons import { ProfileIcon } from '@/assets/Icon'; +// services +import { BASE_URL } from '@/service/base/api'; +import { userManager } from '@/service/user'; + +// types +import { ModalType, errorMessage } from '@/constants/constant'; + export default function ProfileButton() { + const user = useSelector(selectUser); + const dispatch = useDispatch(); + + const router = useRouter(); + + const { openModal } = useModal(); + + const [dropdownOpen, setDropdownOpen] = useState(false); + + const handleLogout = async () => { + try { + await userManager.logoutUser(); + dispatch(logOut()); + router.replace('/'); + } catch { + openModal({ + type: ModalType.ERROR, + message: errorMessage.tryAgain, + }); + } + }; + return ( - ); } diff --git a/client/src/components/Navigator/index.tsx b/client/src/components/Navigator/index.tsx index dbf32523..4507d5f7 100644 --- a/client/src/components/Navigator/index.tsx +++ b/client/src/components/Navigator/index.tsx @@ -1,64 +1,27 @@ -import Image from 'next/image'; -import Link from 'next/link'; -import Button from '../base/Button'; -import SearchButton from './SearchButton'; -import ChatButton from './ChatButton'; -import AlarmButton from './AlarmButton'; -import ProfileButton from './ProfileButton'; -import { ArrowDownIcon } from '@/assets/Icon'; -import PostModeNavigation from './PostModeNavigation'; +//components +import NavigatorTop from './NavigatorTop'; +import NavigatorBottom from './NavigatorBottom'; + +//hooks +import useServerLoginCheck from '@/hooks/common/useServerLoginCheck'; + +//types import { PostViewType } from '@/types'; interface NavitatorProps { viewMode?: PostViewType; } export default function Navigator({ viewMode }: NavitatorProps) { + const initialUserLogin = useServerLoginCheck(); + return (
-
- - - reDuck - - reDuck-icon - - -
-
    -
  • - -
  • -
  • - -
  • -
  • - -
  • -
- - - -
-
- -
-
- - -
-
+ +
); diff --git a/client/src/components/Tooltip/index.tsx b/client/src/components/Tooltip/index.tsx new file mode 100644 index 00000000..5175f925 --- /dev/null +++ b/client/src/components/Tooltip/index.tsx @@ -0,0 +1,17 @@ +import { PropsWithChildren } from 'react'; + +interface IProps extends PropsWithChildren { + tooltipText: string; +} + +export default function Tooltip({ tooltipText, children }: IProps) { + //TODO: 필요시 props로 포지션을 받아 스타일 구현 + return ( +
+ {children} +
+ {tooltipText} +
+
+ ); +} diff --git a/client/src/components/index.tsx b/client/src/components/index.tsx index e47728aa..d8fc8dc1 100644 --- a/client/src/components/index.tsx +++ b/client/src/components/index.tsx @@ -12,3 +12,5 @@ export { default as Skeleton } from './Skeleton'; export { default as Form } from './Form'; export { default as Alert } from './Alert'; export { default as Button } from './base/Button'; +export { default as Tooltip } from './Tooltip'; +export { default as Dropdown } from './Dropdown'; diff --git a/client/src/hooks/common/useClickAwayRef.ts b/client/src/hooks/common/useClickAwayRef.ts new file mode 100644 index 00000000..a4a4fc54 --- /dev/null +++ b/client/src/hooks/common/useClickAwayRef.ts @@ -0,0 +1,37 @@ +import { useEffect, useRef } from 'react'; + +const events: Array<'mousedown' | 'touchstart'> = ['mousedown', 'touchstart']; + +type ClickEvent = MouseEvent | TouchEvent; + +const useClickAwayRef = (handler: (e: ClickEvent) => void) => { + const ref = useRef(null); + const saveHandler = useRef(handler); + + useEffect(() => { + saveHandler.current = handler; + }, [handler]); + + useEffect(() => { + const element = ref.current; + if (!element) return; + + const handleEvent = (e: ClickEvent) => { + !element.contains(e.target as HTMLElement) && saveHandler.current(e); + }; + + for (const eventName of events) { + document.addEventListener(eventName, handleEvent); + } + + return () => { + for (const eventName of events) { + document.removeEventListener(eventName, handleEvent); + } + }; + }, [ref]); + + return ref; +}; + +export default useClickAwayRef; diff --git a/client/src/hooks/common/useClientLoginCheck.ts b/client/src/hooks/common/useClientLoginCheck.ts new file mode 100644 index 00000000..49cea932 --- /dev/null +++ b/client/src/hooks/common/useClientLoginCheck.ts @@ -0,0 +1,19 @@ +import { useState, useEffect } from 'react'; + +//redux +import { useSelector } from 'react-redux'; +import { selectUser } from '@/lib/redux/features/auth/authSlice'; + +const useClientLoginCheck = (initialUserLogin: boolean) => { + const [userLogin, setUserLogin] = useState(initialUserLogin); + + const user = useSelector(selectUser); + + useEffect(() => { + setUserLogin(!!user.userId); + }, [user]); + + return userLogin; +}; + +export default useClientLoginCheck; diff --git a/client/src/hooks/common/useServerLoginCheck.ts b/client/src/hooks/common/useServerLoginCheck.ts new file mode 100644 index 00000000..3b3e3abb --- /dev/null +++ b/client/src/hooks/common/useServerLoginCheck.ts @@ -0,0 +1,14 @@ +'use server'; + +import { cookies } from 'next/headers'; + +const useServerLoginCheck = () => { + const cookieStore = cookies(); + const token = cookieStore.get('token')?.value; + + const initialUserLogin = !!token; + + return initialUserLogin; +}; + +export default useServerLoginCheck; diff --git a/client/src/lib/redux/store.ts b/client/src/lib/redux/store.ts index 8e0a3c4f..07cb2e9a 100644 --- a/client/src/lib/redux/store.ts +++ b/client/src/lib/redux/store.ts @@ -1,14 +1,46 @@ import { configureStore } from '@reduxjs/toolkit'; import { reducer } from './features/index'; +//redux-persist +import { persistStore, persistReducer } from 'redux-persist'; +import createWebStorage from 'redux-persist/lib/storage/createWebStorage'; + +const createNoopStorage = () => { + return { + getItem() { + return Promise.resolve(null); + }, + setItem(value: string) { + return Promise.resolve(value); + }, + removeItem() { + return Promise.resolve(); + }, + }; +}; + +const storage = + typeof window === 'undefined' + ? createNoopStorage() + : createWebStorage('local'); + +const persistConfig = { + key: 'root', + storage, + whitelist: ['auth'], +}; + +const persistedReducer = persistReducer(persistConfig, reducer); + export const makeStore = () => { return configureStore({ - reducer, + reducer: persistedReducer, middleware: (getDefaultMiddleware) => getDefaultMiddleware({ serializableCheck: false }), }); }; export const store = makeStore(); +export const persistor = persistStore(store); export type AppStore = ReturnType; diff --git a/client/src/lib/redux/store/index.ts b/client/src/lib/redux/store/index.ts deleted file mode 100644 index 96a1723b..00000000 --- a/client/src/lib/redux/store/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -'use client'; -import { Action, ThunkAction, configureStore } from '@reduxjs/toolkit'; -import { reducer } from '../features'; -import { createWrapper } from 'next-redux-wrapper'; - -const makeStore = () => - configureStore({ - reducer, - middleware: (getDefaultMiddleware) => - getDefaultMiddleware({ serializableCheck: false }), - }); - -export type RootStore = ReturnType; -export type RootState = ReturnType; -export type RootThunk = ThunkAction< - ReturnType, - RootState, - unknown, - Action ->; - -export const wrapper = createWrapper(makeStore); diff --git a/client/yarn.lock b/client/yarn.lock index 077eb7aa..3a0d9dd4 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -7992,11 +7992,6 @@ neo-async@^2.5.0, neo-async@^2.6.2: resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== -next-redux-wrapper@^8.1.0: - version "8.1.0" - resolved "https://registry.yarnpkg.com/next-redux-wrapper/-/next-redux-wrapper-8.1.0.tgz#d9c135f1ceeb2478375bdacd356eb9db273d3a07" - integrity sha512-2hIau0hcI6uQszOtrvAFqgc0NkZegKYhBB7ZAKiG3jk7zfuQb4E7OV9jfxViqqojh3SEHdnFfPkN9KErttUKuw== - next@^14.0.4: version "14.0.4" resolved "https://registry.yarnpkg.com/next/-/next-14.0.4.tgz#bf00b6f835b20d10a5057838fa2dfced1d0d84dc" @@ -9410,6 +9405,11 @@ redux-devtools-extension@^2.13.9: resolved "https://registry.yarnpkg.com/redux-devtools-extension/-/redux-devtools-extension-2.13.9.tgz#6b764e8028b507adcb75a1cae790f71e6be08ae7" integrity sha512-cNJ8Q/EtjhQaZ71c8I9+BPySIBVEKssbPpskBfsXqb8HJ002A3KRVHfeRzwRo6mGPqsm7XuHTqNSNeS1Khig0A== +redux-persist@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/redux-persist/-/redux-persist-6.0.0.tgz#b4d2972f9859597c130d40d4b146fecdab51b3a8" + integrity sha512-71LLMbUq2r02ng2We9S215LtPu3fY0KgaGE0k8WRgl6RkqxtGfl7HUozz1Dftwsb0D/5mZ8dwAaPbtnzfvbEwQ== + redux-thunk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.4.2.tgz#b9d05d11994b99f7a91ea223e8b04cf0afa5ef3b"