diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx index 77e2aa5..f9d735d 100644 --- a/src/components/Header/Header.tsx +++ b/src/components/Header/Header.tsx @@ -3,12 +3,14 @@ import { Flex, Box } from '@chakra-ui/react'; import AppName from './AppName'; import UserInfo from './User/UserInfo'; import UserNotifications from './User/UserNotifications'; -import useWindowDimensions from 'hooks/useWindowDimensions'; import NavMobile from '../Nav/Mobile/Nav'; import { observer } from 'mobx-react'; +import { useRootStore } from 'hooks/useRootStore'; const Header: React.FC = () => { - const { isDesktop } = useWindowDimensions(); + const { + uiStore: { isDesktop }, + } = useRootStore(); return ( { - const { authStore, userStore } = useRootStore(); - const history = useHistory(); - const toast = useToast(); + const { userStore } = useRootStore(); + const { logout } = useLogoutQuery(); - const onLogoutHandler = async (e: React.MouseEvent) => { - try { - await authStore.logout(); - history.push('/auth'); - toast({ title: 'You are logged out.', isClosable: true, status: 'info' }); - } catch (error) { - console.error(error); - } - }; + // const onLogoutHandler = async (e: React.MouseEvent) => { + // try { + // await authStore.logout(); + // history.push('/auth'); + // toast({ title: 'You are logged out.', isClosable: true, status: 'info' }); + // } catch (error) { + // console.error(error); + // } + // }; if (userStore.me === null) { return ; @@ -60,7 +59,7 @@ const UserInfo: React.FC = () => { My profile - + Logout diff --git a/src/components/Nav/Desktop/Nav.tsx b/src/components/Nav/Desktop/Nav.tsx index 9a2e534..ff0f9ef 100644 --- a/src/components/Nav/Desktop/Nav.tsx +++ b/src/components/Nav/Desktop/Nav.tsx @@ -1,7 +1,8 @@ import React from 'react'; import { Flex, Box } from '@chakra-ui/react'; -import useWindowDimensions from 'hooks/useWindowDimensions'; import NavList from '../NavList'; +import { useRootStore } from 'hooks/useRootStore'; +import { observer } from 'mobx-react'; /** * @@ -9,7 +10,9 @@ import NavList from '../NavList'; * Component for navigating the application. */ const Nav: React.FC = () => { - const { isDesktop } = useWindowDimensions(); + const { + uiStore: { isDesktop }, + } = useRootStore(); if (isDesktop) { return ( @@ -32,4 +35,4 @@ const Nav: React.FC = () => { return null; }; -export default Nav; +export default observer(Nav); diff --git a/src/components/Nav/Mobile/Nav.tsx b/src/components/Nav/Mobile/Nav.tsx index c200f3e..6a38d6d 100644 --- a/src/components/Nav/Mobile/Nav.tsx +++ b/src/components/Nav/Mobile/Nav.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { observer } from 'mobx-react'; +import { MdExitToApp } from 'react-icons/md'; import { Link } from 'react-router-dom'; import { useRootStore } from 'hooks/useRootStore'; @@ -47,6 +48,7 @@ const NavMobile: React.FC = () => { View profile + } /> ) : ( diff --git a/src/components/PrivateRoute/PrivateRoute.tsx b/src/components/PrivateRoute/PrivateRoute.tsx index b94d6a4..063e0fe 100644 --- a/src/components/PrivateRoute/PrivateRoute.tsx +++ b/src/components/PrivateRoute/PrivateRoute.tsx @@ -4,7 +4,6 @@ import Layout from '../Layout/Layout'; import { Route, Redirect } from 'react-router-dom'; import { IPrivateRouteProps } from 'interfaces'; import { useRootStore } from 'hooks/useRootStore'; -import { observer } from 'mobx-react'; import { ErrorBoundary } from 'react-error-boundary'; import ErrorFallback from '../ErrorFallback'; import config from 'config'; @@ -39,4 +38,4 @@ const PrivateRoute: React.FC = ({ component: Component, titl ); }; -export default observer(PrivateRoute); +export default PrivateRoute; diff --git a/src/components/User/UserBadge.tsx b/src/components/User/UserBadge.tsx index 5848790..19fb6a7 100644 --- a/src/components/User/UserBadge.tsx +++ b/src/components/User/UserBadge.tsx @@ -2,14 +2,13 @@ import { Badge } from '@chakra-ui/react'; import React from 'react'; import { UserRoleEnum } from 'enums'; import { IUserRole } from 'interfaces'; +import { userHasRoles } from 'helpers'; /** * Returns the badge for user based on roles */ const UserBadge: React.FC<{ roles: IUserRole[] }> = ({ roles }) => { - const slugs = roles.map(role => role.slug); - - if (slugs.includes(UserRoleEnum.ADMIN)) { + if (userHasRoles(roles, [UserRoleEnum.ADMIN])) { return ( Administrator @@ -17,7 +16,7 @@ const UserBadge: React.FC<{ roles: IUserRole[] }> = ({ roles }) => { ); } - if (slugs.includes(UserRoleEnum.TEACHER)) { + if (userHasRoles(roles, [UserRoleEnum.TEACHER])) { return ( Teacher @@ -25,7 +24,7 @@ const UserBadge: React.FC<{ roles: IUserRole[] }> = ({ roles }) => { ); } - if (slugs.includes(UserRoleEnum.STUDENT)) { + if (userHasRoles(roles, [UserRoleEnum.STUDENT])) { return Student; } diff --git a/src/hooks/useLogoutQuery.ts b/src/hooks/useLogoutQuery.ts new file mode 100644 index 0000000..e630d1d --- /dev/null +++ b/src/hooks/useLogoutQuery.ts @@ -0,0 +1,49 @@ +import React from 'react'; +import { useToast } from '@chakra-ui/react'; +import { useHistory } from 'react-router-dom'; +import { useRootStore } from './useRootStore'; +import { IDataResult } from 'interfaces'; +import { useErrorHandler } from 'react-error-boundary'; + +type StateType = { + result: IDataResult | null; + error: any; + loading: boolean; + fetched: boolean; +}; + +const useLogoutQuery = () => { + const { authStore } = useRootStore(); + const [state, setState] = React.useState({ result: null, error: null, loading: false, fetched: false }); + const isMounted = React.useRef(true); + const history = useHistory(); + const toast = useToast(); + const handleError = useErrorHandler(); + const request = React.useCallback(async () => { + setState(s => ({ ...s, loading: true })); + try { + const data = await authStore.logout(); + history.push('/auth'); + + toast({ title: 'You are logged out.', isClosable: true, status: 'info' }); + if (isMounted.current) setState(s => ({ ...s, result: data })); + } catch (error) { + handleError(error); + setState(s => ({ ...s, error })); + } finally { + if (isMounted.current) setState(s => ({ ...s, loading: false, fetched: true })); + } + }, [authStore, history, toast, handleError]); + + const logout = () => request(); + + React.useEffect(() => { + return () => { + isMounted.current = false; + }; + }, []); + + return { ...state, logout }; +}; + +export default useLogoutQuery; diff --git a/src/hooks/useWindowDimensions.ts b/src/hooks/useWindowDimensions.ts deleted file mode 100644 index f20b16b..0000000 --- a/src/hooks/useWindowDimensions.ts +++ /dev/null @@ -1,32 +0,0 @@ -import React from 'react'; - -const useWindowDimensions = () => { - const [windowDimensions, setWindowDimensions] = React.useState({ - width: window.innerWidth, - height: window.innerHeight, - }); - - React.useEffect(() => { - /** - * Update state on resize - */ - const handleResize = () => { - setWindowDimensions({ width: window.innerWidth, height: window.innerHeight }); - }; - - window.addEventListener('resize', handleResize); - - return () => { - window.removeEventListener('resize', handleResize); - }; - }, []); - - return { - windowDimensions, - isDesktop: windowDimensions.width > 768, - isTablet: windowDimensions.width < 768, - isMobile: windowDimensions.width < 568, - }; -}; - -export default useWindowDimensions; diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts index 6a7b034..65e71d7 100644 --- a/src/interfaces/index.ts +++ b/src/interfaces/index.ts @@ -71,3 +71,8 @@ export interface IPublicRouteProps extends RouteProps { component: React.FC; title?: string; } + +export interface IWindowDimensions { + width: number; + height: number; +} diff --git a/src/pages/courses.tsx b/src/pages/courses.tsx index 0b669d2..541c11b 100644 --- a/src/pages/courses.tsx +++ b/src/pages/courses.tsx @@ -1,23 +1,12 @@ -import React, { useEffect } from 'react'; +import React from 'react'; import { Box, Heading, Breadcrumb, BreadcrumbItem, BreadcrumbLink, Grid } from '@chakra-ui/react'; import { IPageProps } from 'interfaces'; -import { useRootStore } from 'hooks/useRootStore'; -import { useHistory } from 'react-router-dom'; /** * Courses page */ const CoursesPage: React.FC = ({ title }) => { - const { authStore } = useRootStore(); - const history = useHistory(); - - useEffect(() => { - if (!authStore.isLoggedIn) { - history.push('/auth'); - } - }); - return ( <> diff --git a/src/pages/messages.tsx b/src/pages/messages.tsx index ac14eef..b638430 100644 --- a/src/pages/messages.tsx +++ b/src/pages/messages.tsx @@ -1,23 +1,12 @@ -import React, { useEffect } from 'react'; +import React from 'react'; import { Box, Heading, Breadcrumb, BreadcrumbItem, BreadcrumbLink, Grid } from '@chakra-ui/react'; import { IPageProps } from 'interfaces'; -import { useRootStore } from 'hooks/useRootStore'; -import { useHistory } from 'react-router-dom'; /** * Messages page */ const MessagesPage: React.FC = ({ title }) => { - const { authStore } = useRootStore(); - const history = useHistory(); - - useEffect(() => { - if (!authStore.isLoggedIn) { - history.push('/auth'); - } - }); - return ( <> diff --git a/src/stores/UIStore.ts b/src/stores/UIStore.ts index 43d9d9c..c34b068 100644 --- a/src/stores/UIStore.ts +++ b/src/stores/UIStore.ts @@ -1,6 +1,7 @@ -import { makeAutoObservable } from 'mobx'; +import { makeAutoObservable, observable } from 'mobx'; import { createBrowserHistory } from 'history'; import RootStore from './RootStore'; +import { IWindowDimensions } from 'interfaces'; export default class UIStore { public root: RootStore; @@ -9,18 +10,43 @@ export default class UIStore { public history: ReturnType; + public windowDimensions: IWindowDimensions = { width: window.innerWidth, height: window.innerHeight }; + constructor(root: RootStore) { this.root = root; this.history = createBrowserHistory(); - makeAutoObservable(this); + + makeAutoObservable(this, { + windowDimensions: observable.struct, + }); this.history.listen(({ pathname }) => { console.log(`[history]: pathname changed to "${pathname}"`); this.location = pathname; }); + + window.onresize = () => { + this.windowDimensions = this.getWindowDimensions(); + }; } - public setLocation(location: string) { + public setLocation(location: string): void { this.location = location; } + + private getWindowDimensions(): IWindowDimensions { + return { width: window.innerWidth, height: window.innerHeight }; + } + + get isDesktop() { + return this.windowDimensions.width > 768; + } + + get isTablet() { + return this.windowDimensions.width < 768; + } + + get isMobile() { + return this.windowDimensions.width < 568; + } }