diff --git a/src/app.jsx b/src/app.jsx index 6e646e6..cb928d6 100644 --- a/src/app.jsx +++ b/src/app.jsx @@ -1,16 +1,14 @@ -import React from 'react'; -import { Provider, useSelector } from 'react-redux'; +import React, { useContext } from 'react'; import { Router } from 'react-router'; import { Switch, Route, Redirect } from 'react-router-dom'; import history from 'services/history.service'; import * as loaderService from 'services/loader.service'; import * as socketService from 'services/socket.service'; +import api from 'services/api.service'; -import store from 'resources/store'; - -import * as userSelectors from 'resources/user/user.selectors'; -import { userActions } from 'resources/user/user.slice'; +import ContextStoreProvider, { StoreContext } from 'resources/store/store'; +import actions from 'resources/store/actions'; import Toast from 'components/toast'; import Loading from 'components/loading'; @@ -26,19 +24,33 @@ import Reset from 'pages/reset'; import Home from 'pages/home'; import NotFound from 'pages/not-found'; -import 'resources/user/user.handlers'; - import 'styles/main.pcss'; const Profile = React.lazy(() => import('./pages/profile')); function PrivateScope({ children }) { - const user = useSelector(userSelectors.selectUser); + const { state, dispatch } = useContext(StoreContext); + const { user } = state; React.useEffect(() => { socketService.connect(); + + api.on('error', (error) => { + if (error.status === 401) { + dispatch(actions.signOut()); + } + }); + + socketService.on('connect', () => { + socketService.emit('subscribe', `user-${user?._id}`); + }); + + socketService.on('user:updated', (updatedUser) => { + dispatch(actions.setUser({ user: updatedUser })); + }); + return () => socketService.disconnect(); - }, []); + }, [user?._id, dispatch]); if (!user) { const searchParams = new URLSearchParams({ to: window.location.pathname }); @@ -86,11 +98,12 @@ Object.values(scope).forEach((s, scopeIndex) => { function App() { const [loading, setLoading] = React.useState(true); + const { dispatch } = useContext(StoreContext); React.useEffect(() => { async function init() { try { - await store.dispatch(userActions.getCurrentUser()); + await dispatch(actions.getCurrentUser()); } catch (error) { // @todo: add something like sentry } finally { @@ -100,12 +113,13 @@ function App() { } init(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); if (loading) return null; return ( - + <> Error!}> }> @@ -135,8 +149,14 @@ function App() { - + ); } -export default App; +const Root = () => ( + + + +); + +export default Root; diff --git a/src/components/toast/toast.jsx b/src/components/toast/toast.jsx index 8374d89..e866d56 100644 --- a/src/components/toast/toast.jsx +++ b/src/components/toast/toast.jsx @@ -1,10 +1,9 @@ -import React from 'react'; +import React, { useContext } from 'react'; import cn from 'classnames'; import ReactDOM from 'react-dom'; -import { useSelector, useDispatch } from 'react-redux'; -import * as toastSelectors from 'resources/toast/toast.selectors'; -import { toastActions } from 'resources/toast/toast.slice'; +import { StoreContext } from 'resources/store/store'; +import actions from 'resources/store/actions'; import Icon from 'components/icon'; import IconButton from 'components/icon-button'; @@ -39,9 +38,8 @@ function getIconProps(type) { } function RawToast() { - const dispatch = useDispatch(); - - const messages = useSelector(toastSelectors.selectMessages); + const { state, dispatch } = useContext(StoreContext); + const { toast: messages } = state; const element = React.useRef(document.createElement('div')); @@ -57,16 +55,16 @@ function RawToast() { function list() { return ( <> - {messages.map((m) => { + {messages.map(({ id, message, type }) => { const closeToast = () => { - dispatch(toastActions.remove({ id: m.id })); + dispatch(actions.removeToast(id)); }; - const iconProps = getIconProps(m.type); + const iconProps = getIconProps(type); return (
- {m.text} + {message}
( - + - + ), ], }; export const Template = () => { - const dispatch = useDispatch(); + const { dispatch } = useContext(StoreContext); const showSuccessToast = () => { - dispatch(toastActions.success('This is success toast! This is success toast!')); + dispatch(actions.createToast({ type: 'success', message: 'This is success toast! This is success toast!' })); }; const showErrorToast = () => { - dispatch(toastActions.error('This is error toast! This is error toast!')); + dispatch(actions.createToast({ type: 'error', message: 'This is error toast! This is error toast!' })); }; const showInfoToast = () => { - dispatch(toastActions.info('This is info toast! This is info toast!')); + dispatch(actions.createToast({ type: 'info', message: 'This is info toast! This is info toast!' })); }; const showWarningToast = () => { - dispatch(toastActions.warning('This is warning toast! This is warning toast!')); + dispatch(actions.createToast({ type: 'warning', message: 'This is warning toast! This is warning toast!' })); }; return ( diff --git a/src/layouts/main/header/components/user-menu/user-menu.jsx b/src/layouts/main/header/components/user-menu/user-menu.jsx index 635362e..de8f18d 100644 --- a/src/layouts/main/header/components/user-menu/user-menu.jsx +++ b/src/layouts/main/header/components/user-menu/user-menu.jsx @@ -1,11 +1,11 @@ -import React from 'react'; +import React, { useContext } from 'react'; import cn from 'classnames'; import { Link, useHistory } from 'react-router-dom'; -import { useDispatch } from 'react-redux'; import { routes } from 'routes'; -import { userActions } from 'resources/user/user.slice'; +import { StoreContext } from 'resources/store/store'; +import actions from 'resources/store/actions'; import styles from './user-menu.styles.pcss'; @@ -17,8 +17,8 @@ const linksList = [ ]; function UserMenu() { - const dispatch = useDispatch(); const history = useHistory(); + const { dispatch } = useContext(StoreContext); const [isOpen, setIsOpen] = React.useState(false); function toggleIsOpen() { @@ -36,7 +36,7 @@ function UserMenu() { }, []); async function logout() { - await dispatch(userActions.signOut()); + await dispatch(actions.signOut()); history.push(routes.signIn.path); } diff --git a/src/pages/forgot/forgot.jsx b/src/pages/forgot/forgot.jsx index 52a8bdb..1880797 100644 --- a/src/pages/forgot/forgot.jsx +++ b/src/pages/forgot/forgot.jsx @@ -1,12 +1,12 @@ -import React from 'react'; +import React, { useContext } from 'react'; import cn from 'classnames'; import { Link, Redirect } from 'react-router-dom'; -import { useDispatch, useSelector } from 'react-redux'; import { routes } from 'routes'; -import * as userSelectors from 'resources/user/user.selectors'; -import { userActions } from 'resources/user/user.slice'; +import { StoreContext } from 'resources/store/store'; + +import * as api from 'resources/user/user.api'; import Input from 'components/input'; import Button from 'components/button'; @@ -15,18 +15,17 @@ import Form from 'components/form'; import styles from './forgot.pcss'; const Forgot = () => { - const dispatch = useDispatch(); - - const user = useSelector(userSelectors.selectUser); + const { state } = useContext(StoreContext); + const { user } = state; const [values, setValues] = React.useState({}); const [pending, setPending] = React.useState(false); const [submitted, setSubmitted] = React.useState(false); - const handleSubmit = async (submitValues) => { + const handleSubmit = async ({ email }) => { setPending(true); - await dispatch(userActions.forgot(submitValues)); - setValues(submitValues); + await api.forgot({ email }); + setValues({ email }); setSubmitted(true); setPending(false); }; diff --git a/src/pages/profile/profile.jsx b/src/pages/profile/profile.jsx index 20aa13d..fda80d7 100644 --- a/src/pages/profile/profile.jsx +++ b/src/pages/profile/profile.jsx @@ -1,11 +1,12 @@ -import React, { useCallback, useState, useEffect } from 'react'; +import React, { + useCallback, useState, useEffect, useContext, +} from 'react'; import * as yup from 'yup'; -import { useDispatch, useSelector } from 'react-redux'; import { useFormContext } from 'react-hook-form'; -import * as userSelectors from 'resources/user/user.selectors'; -import { userActions } from 'resources/user/user.slice'; -import { toastActions } from 'resources/toast/toast.slice'; +import { StoreContext } from 'resources/store/store'; +import actions from 'resources/store/actions'; + import * as filesApi from 'resources/files/files.api'; import Input from 'components/input'; @@ -30,9 +31,8 @@ const schema = yup.object({ .email('Please enter a valid email address'), }); -const CancelButton = ({ onCancel }) => { // eslint-disable-line react/prop-types +const CancelButton = ({ onCancel, user }) => { // eslint-disable-line react/prop-types const { reset } = useFormContext(); - const user = useSelector(userSelectors.selectUser); const handleClick = () => { onCancel(); @@ -52,16 +52,15 @@ const CancelButton = ({ onCancel }) => { // eslint-disable-line react/prop-types }; const Profile = () => { - const dispatch = useDispatch(); - const user = useSelector(userSelectors.selectUser); - const { avatarFileKey: userAvatarFileKey } = user; + const { state: { user }, dispatch } = useContext(StoreContext); + const { avatarFileKey: userAvatarFileKey } = user || {}; const [avatarFileKey, setAvatarFileKey] = useState(userAvatarFileKey); const [avatarUrl, setAvatarUrl] = useState(null); const handleSubmit = useCallback(async (submitValues) => { - await dispatch(userActions.updateCurrentUser({ ...submitValues, avatarFileKey })); - dispatch(toastActions.success('User info updated!')); + await dispatch(actions.updateCurrentUser({ ...submitValues, avatarFileKey })); + dispatch(actions.createToast({ type: 'success', message: 'User info updated!' })); }, [dispatch, avatarFileKey]); const getAvatarUrl = useCallback(async (key) => { @@ -70,8 +69,8 @@ const Profile = () => { if (url) { setAvatarUrl(url); } - } catch (error) { - dispatch(toastActions.error(error)); + } catch ({ data }) { + dispatch(actions.createToast({ type: 'error', message: data.errors.file[0] })); } }, [dispatch]); @@ -80,8 +79,8 @@ const Profile = () => { const file = files[0]; const { key } = await filesApi.upload(file); setAvatarFileKey(key); - } catch (error) { - dispatch(toastActions.error(error)); + } catch ({ data }) { + dispatch(actions.createToast({ type: 'error', message: data.errors.file[0] })); } }; @@ -162,6 +161,7 @@ const Profile = () => {